Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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 |
Jim Tang | d740fa5 | 2019-03-14 16:07:08 +0800 | [diff] [blame^] | 25 | import atest_utils |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 26 | import constants |
yelinhsieh | 6dc7f14 | 2018-08-31 16:09:39 +0800 | [diff] [blame] | 27 | from test_finders import test_info |
| 28 | from test_finders import test_finder_base |
| 29 | from test_finders import test_finder_utils |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 30 | from test_runners import atest_tf_test_runner |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 31 | from test_runners import robolectric_test_runner |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 32 | from test_runners import vts_tf_test_runner |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 33 | |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 34 | _CC_EXT_RE = re.compile(r'.*(\.cc|\.cpp)$', re.I) |
yelinhsieh | e4580a5 | 2018-08-16 15:51:54 +0800 | [diff] [blame] | 35 | _JAVA_EXT_RE = re.compile(r'.*(\.java|\.kt)$', re.I) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 36 | |
| 37 | _MODULES_IN = 'MODULES-IN-%s' |
| 38 | _ANDROID_MK = 'Android.mk' |
| 39 | |
| 40 | # These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so |
| 41 | # we can ignore them. |
| 42 | _SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'}) |
| 43 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 44 | class ModuleFinder(test_finder_base.TestFinderBase): |
| 45 | """Module finder class.""" |
| 46 | NAME = 'MODULE' |
| 47 | _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 48 | _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 49 | _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 50 | |
| 51 | def __init__(self, module_info=None): |
| 52 | super(ModuleFinder, self).__init__() |
| 53 | self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) |
| 54 | self.module_info = module_info |
| 55 | |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 56 | def _determine_testable_module(self, path): |
| 57 | """Determine which module the user is trying to test. |
| 58 | |
| 59 | Returns the module to test. If there are multiple possibilities, will |
| 60 | ask the user. Otherwise will return the only module found. |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 61 | |
| 62 | Args: |
| 63 | path: String path of module to look for. |
| 64 | |
| 65 | Returns: |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 66 | String of the module name. |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 67 | """ |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 68 | testable_modules = [] |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 69 | for mod in self.module_info.get_module_names(path): |
| 70 | mod_info = self.module_info.get_module_info(mod) |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 71 | # Robolectric tests always exist in pairs of 2, one module to build |
| 72 | # the test and another to run it. For now, we are assuming they are |
| 73 | # isolated in their own folders and will return if we find one. |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 74 | if self.module_info.is_robolectric_test(mod): |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 75 | return mod |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 76 | if self.module_info.is_testable_module(mod_info): |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 77 | testable_modules.append(mod_info.get(constants.MODULE_NAME)) |
| 78 | return test_finder_utils.extract_test_from_tests(testable_modules) |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 79 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 80 | def _is_vts_module(self, module_name): |
| 81 | """Returns True if the module is a vts module, else False.""" |
| 82 | mod_info = self.module_info.get_module_info(module_name) |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 83 | suites = [] |
| 84 | if mod_info: |
| 85 | suites = mod_info.get('compatibility_suites', []) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 86 | # Pull out all *ts (cts, tvts, etc) suites. |
| 87 | suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE] |
| 88 | return len(suites) == 1 and 'vts' in suites |
| 89 | |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 90 | def _update_to_vts_test_info(self, test): |
| 91 | """Fill in the fields with vts specific info. |
| 92 | |
| 93 | We need to update the runner to use the vts runner and also find the |
| 94 | test specific depedencies |
| 95 | |
| 96 | Args: |
| 97 | test: TestInfo to update with vts specific details. |
| 98 | |
| 99 | Return: |
| 100 | TestInfo that is ready for the vts test runner. |
| 101 | """ |
| 102 | test.test_runner = self._VTS_TEST_RUNNER |
| 103 | config_file = os.path.join(self.root_dir, |
| 104 | test.data[constants.TI_REL_CONFIG]) |
| 105 | # Need to get out dir (special logic is to account for custom out dirs). |
| 106 | # The out dir is used to construct the build targets for the test deps. |
| 107 | out_dir = os.environ.get(constants.ANDROID_HOST_OUT) |
| 108 | custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR) |
| 109 | # If we're not an absolute custom out dir, get relative out dir path. |
| 110 | if custom_out_dir is None or not os.path.isabs(custom_out_dir): |
| 111 | out_dir = os.path.relpath(out_dir, self.root_dir) |
| 112 | vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases') |
kellyhung | d69de38 | 2018-08-07 18:32:58 +0800 | [diff] [blame] | 113 | # Parse dependency of default staging plans. |
yangbill | 22ed51a | 2018-12-04 16:48:24 +0800 | [diff] [blame] | 114 | |
| 115 | xml_path = test_finder_utils.search_integration_dirs( |
| 116 | constants.VTS_STAGING_PLAN, |
| 117 | self.module_info.get_paths(constants.VTS_TF_MODULE)) |
kellyhung | d69de38 | 2018-08-07 18:32:58 +0800 | [diff] [blame] | 118 | vts_xmls = test_finder_utils.get_plans_from_vts_xml(xml_path) |
| 119 | vts_xmls.add(config_file) |
| 120 | for config_file in vts_xmls: |
| 121 | # Add in vts test build targets. |
| 122 | test.build_targets |= test_finder_utils.get_targets_from_vts_xml( |
| 123 | config_file, vts_out_dir, self.module_info) |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 124 | test.build_targets.add('vts-test-core') |
Kevin Cheng | 284aafc | 2018-03-29 09:09:31 -0700 | [diff] [blame] | 125 | test.build_targets.add(test.test_name) |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 126 | return test |
| 127 | |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 128 | def _update_to_robolectric_test_info(self, test): |
| 129 | """Update the fields for a robolectric test. |
| 130 | |
| 131 | Args: |
| 132 | test: TestInfo to be updated with robolectric fields. |
| 133 | |
| 134 | Returns: |
| 135 | TestInfo with robolectric fields. |
| 136 | """ |
| 137 | test.test_runner = self._ROBOLECTRIC_RUNNER |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 138 | test.test_name = self.module_info.get_robolectric_test_name(test.test_name) |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 139 | return test |
| 140 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 141 | def _process_test_info(self, test): |
| 142 | """Process the test info and return some fields updated/changed. |
| 143 | |
| 144 | We need to check if the test found is a special module (like vts) and |
| 145 | update the test_info fields (like test_runner) appropriately. |
| 146 | |
| 147 | Args: |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 148 | test: TestInfo that has been filled out by a find method. |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 149 | |
| 150 | Return: |
Jim Tang | d740fa5 | 2019-03-14 16:07:08 +0800 | [diff] [blame^] | 151 | TestInfo that has been modified as needed and return None if |
nelsonli | e8f76d9 | 2018-08-31 16:21:47 +0800 | [diff] [blame] | 152 | this module can't be found in the module_info. |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 153 | """ |
kellyhung | 0625d17 | 2018-06-21 16:40:27 +0800 | [diff] [blame] | 154 | module_name = test.test_name |
| 155 | mod_info = self.module_info.get_module_info(module_name) |
nelsonli | e8f76d9 | 2018-08-31 16:21:47 +0800 | [diff] [blame] | 156 | if not mod_info: |
| 157 | return None |
kellyhung | 0625d17 | 2018-06-21 16:40:27 +0800 | [diff] [blame] | 158 | test.module_class = mod_info['class'] |
| 159 | test.install_locations = test_finder_utils.get_install_locations( |
| 160 | mod_info['installed']) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 161 | # Check if this is only a vts module. |
| 162 | if self._is_vts_module(test.test_name): |
Kevin Cheng | e084eb0 | 2018-02-12 12:48:35 -0800 | [diff] [blame] | 163 | return self._update_to_vts_test_info(test) |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 164 | elif self.module_info.is_robolectric_test(test.test_name): |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 165 | return self._update_to_robolectric_test_info(test) |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 166 | rel_config = test.data[constants.TI_REL_CONFIG] |
| 167 | test.build_targets = self._get_build_targets(module_name, rel_config) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 168 | return test |
| 169 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 170 | def _get_build_targets(self, module_name, rel_config): |
| 171 | """Get the test deps. |
| 172 | |
| 173 | Args: |
| 174 | module_name: name of the test. |
| 175 | rel_config: XML for the given test. |
| 176 | |
| 177 | Returns: |
| 178 | Set of build targets. |
| 179 | """ |
| 180 | targets = set() |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 181 | if not self.module_info.is_auto_gen_test_config(module_name): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 182 | config_file = os.path.join(self.root_dir, rel_config) |
| 183 | targets = test_finder_utils.get_targets_from_xml(config_file, |
| 184 | self.module_info) |
yangbill | 7310912 | 2018-10-25 20:09:33 +0800 | [diff] [blame] | 185 | for module_path in self.module_info.get_paths(module_name): |
| 186 | mod_dir = module_path.replace('/', '-') |
| 187 | targets.add(_MODULES_IN % mod_dir) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 188 | return targets |
| 189 | |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 190 | def _get_module_test_config(self, module_name, rel_config=None): |
| 191 | """Get the value of test_config in module_info. |
| 192 | |
| 193 | Get the value of 'test_config' in module_info if its |
| 194 | auto_test_config is not true. |
| 195 | In this case, the test_config is specified by user. |
| 196 | If not, return rel_config. |
| 197 | |
| 198 | Args: |
| 199 | module_name: A string of the test's module name. |
| 200 | rel_config: XML for the given test. |
| 201 | |
| 202 | Returns: |
| 203 | A string of test_config path if found, else return rel_config. |
| 204 | """ |
| 205 | mod_info = self.module_info.get_module_info(module_name) |
nelsonli | e8f76d9 | 2018-08-31 16:21:47 +0800 | [diff] [blame] | 206 | if mod_info: |
| 207 | test_config = '' |
| 208 | test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, []) |
| 209 | if test_config_list: |
| 210 | test_config = test_config_list[0] |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 211 | if not self.module_info.is_auto_gen_test_config(module_name) and test_config != '': |
nelsonli | e8f76d9 | 2018-08-31 16:21:47 +0800 | [diff] [blame] | 212 | return test_config |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 213 | return rel_config |
| 214 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 215 | def find_test_by_module_name(self, module_name): |
| 216 | """Find test for the given module name. |
| 217 | |
| 218 | Args: |
| 219 | module_name: A string of the test's module name. |
| 220 | |
| 221 | Returns: |
| 222 | A populated TestInfo namedtuple if found, else None. |
| 223 | """ |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 224 | mod_info = self.module_info.get_module_info(module_name) |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 225 | if self.module_info.is_testable_module(mod_info): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 226 | # path is a list with only 1 element. |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 227 | rel_config = os.path.join(mod_info['path'][0], |
| 228 | constants.MODULE_CONFIG) |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 229 | rel_config = self._get_module_test_config(module_name, rel_config=rel_config) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 230 | return self._process_test_info(test_info.TestInfo( |
| 231 | test_name=module_name, |
| 232 | test_runner=self._TEST_RUNNER, |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 233 | build_targets=set(), |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 234 | data={constants.TI_REL_CONFIG: rel_config, |
| 235 | constants.TI_FILTER: frozenset()})) |
| 236 | return None |
| 237 | |
| 238 | def find_test_by_class_name(self, class_name, module_name=None, |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 239 | rel_config=None, is_native_test=False): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 240 | """Find test files given a class name. |
| 241 | |
| 242 | If module_name and rel_config not given it will calculate it determine |
| 243 | it by looking up the tree from the class file. |
| 244 | |
| 245 | Args: |
| 246 | class_name: A string of the test's class name. |
| 247 | module_name: Optional. A string of the module name to use. |
| 248 | rel_config: Optional. A string of module dir relative to repo root. |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 249 | is_native_test: A boolean variable of whether to search for a |
| 250 | native test or not. |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 251 | |
| 252 | Returns: |
| 253 | A populated TestInfo namedtuple if test found, else None. |
| 254 | """ |
| 255 | class_name, methods = test_finder_utils.split_methods(class_name) |
| 256 | if rel_config: |
| 257 | search_dir = os.path.join(self.root_dir, |
| 258 | os.path.dirname(rel_config)) |
| 259 | else: |
| 260 | search_dir = self.root_dir |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 261 | test_path = test_finder_utils.find_class_file(search_dir, class_name, |
| 262 | is_native_test) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 263 | if not test_path and rel_config: |
| 264 | logging.info('Did not find class (%s) under module path (%s), ' |
| 265 | 'researching from repo root.', class_name, rel_config) |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 266 | test_path = test_finder_utils.find_class_file(self.root_dir, |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 267 | class_name, |
| 268 | is_native_test) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 269 | if not test_path: |
| 270 | return None |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 271 | if is_native_test: |
| 272 | test_filter = frozenset([test_info.TestFilter( |
| 273 | test_finder_utils.get_cc_filter(class_name, methods), frozenset())]) |
| 274 | else: |
| 275 | full_class_name = test_finder_utils.get_fully_qualified_class_name( |
| 276 | test_path) |
| 277 | test_filter = frozenset([test_info.TestFilter(full_class_name, |
| 278 | methods)]) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 279 | if not rel_config: |
| 280 | test_dir = os.path.dirname(test_path) |
| 281 | rel_module_dir = test_finder_utils.find_parent_module_dir( |
Simran Basi | 0da58b1 | 2018-02-28 17:47:30 -0800 | [diff] [blame] | 282 | self.root_dir, test_dir, self.module_info) |
kellyhung | 2cef0e9 | 2018-07-31 11:15:40 +0800 | [diff] [blame] | 283 | if not rel_module_dir: |
| 284 | return None |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 285 | rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
| 286 | if not module_name: |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 287 | module_name = self._determine_testable_module(os.path.dirname( |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 288 | rel_config)) |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 289 | # The real test config might be record in module-info. |
| 290 | rel_config = self._get_module_test_config(module_name, rel_config=rel_config) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 291 | return self._process_test_info(test_info.TestInfo( |
| 292 | test_name=module_name, |
| 293 | test_runner=self._TEST_RUNNER, |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 294 | build_targets=set(), |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 295 | data={constants.TI_FILTER: test_filter, |
| 296 | constants.TI_REL_CONFIG: rel_config})) |
| 297 | |
| 298 | def find_test_by_module_and_class(self, module_class): |
| 299 | """Find the test info given a MODULE:CLASS string. |
| 300 | |
| 301 | Args: |
| 302 | module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD. |
| 303 | |
| 304 | Returns: |
| 305 | A populated TestInfo namedtuple if found, else None. |
| 306 | """ |
| 307 | if ':' not in module_class: |
| 308 | return None |
| 309 | module_name, class_name = module_class.split(':') |
| 310 | module_info = self.find_test_by_module_name(module_name) |
| 311 | if not module_info: |
| 312 | return None |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 313 | # Find by java class. |
| 314 | find_result = self.find_test_by_class_name( |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 315 | class_name, module_info.test_name, |
| 316 | module_info.data.get(constants.TI_REL_CONFIG)) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 317 | # Find by cc class. |
| 318 | if not find_result: |
| 319 | find_result = self.find_test_by_cc_class_name( |
| 320 | class_name, module_info.test_name, |
| 321 | module_info.data.get(constants.TI_REL_CONFIG)) |
| 322 | return find_result |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 323 | |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 324 | def find_test_by_package_name(self, package, module_name=None, |
| 325 | rel_config=None): |
| 326 | """Find the test info given a PACKAGE string. |
| 327 | |
| 328 | Args: |
| 329 | package: A string of the package name. |
| 330 | module_name: Optional. A string of the module name. |
| 331 | ref_config: Optional. A string of rel path of config. |
| 332 | |
| 333 | Returns: |
| 334 | A populated TestInfo namedtuple if found, else None. |
| 335 | """ |
| 336 | _, methods = test_finder_utils.split_methods(package) |
| 337 | if methods: |
nelsonli | c4a7145 | 2018-09-13 14:10:30 +0800 | [diff] [blame] | 338 | raise atest_error.MethodWithoutClassError('%s: Method filtering ' |
| 339 | 'requires class' % ( |
| 340 | methods)) |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 341 | # Confirm that packages exists and get user input for multiples. |
| 342 | if rel_config: |
| 343 | search_dir = os.path.join(self.root_dir, |
| 344 | os.path.dirname(rel_config)) |
| 345 | else: |
| 346 | search_dir = self.root_dir |
| 347 | package_path = test_finder_utils.run_find_cmd( |
| 348 | test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, |
| 349 | package.replace('.', '/')) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 350 | # Package path will be the full path to the dir represented by package. |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 351 | if not package_path: |
| 352 | return None |
| 353 | test_filter = frozenset([test_info.TestFilter(package, frozenset())]) |
| 354 | if not rel_config: |
| 355 | rel_module_dir = test_finder_utils.find_parent_module_dir( |
Simran Basi | 0da58b1 | 2018-02-28 17:47:30 -0800 | [diff] [blame] | 356 | self.root_dir, package_path, self.module_info) |
kellyhung | 2cef0e9 | 2018-07-31 11:15:40 +0800 | [diff] [blame] | 357 | if not rel_module_dir: |
| 358 | return None |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 359 | rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
| 360 | if not module_name: |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 361 | module_name = self._determine_testable_module( |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 362 | os.path.dirname(rel_config)) |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 363 | # The real test config might be record in module-info. |
| 364 | rel_config = self._get_module_test_config(module_name, rel_config=rel_config) |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 365 | return self._process_test_info(test_info.TestInfo( |
| 366 | test_name=module_name, |
| 367 | test_runner=self._TEST_RUNNER, |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 368 | build_targets=set(), |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 369 | data={constants.TI_FILTER: test_filter, |
| 370 | constants.TI_REL_CONFIG: rel_config})) |
| 371 | |
| 372 | def find_test_by_module_and_package(self, module_package): |
| 373 | """Find the test info given a MODULE:PACKAGE string. |
| 374 | |
| 375 | Args: |
| 376 | module_package: A string of form MODULE:PACKAGE |
| 377 | |
| 378 | Returns: |
| 379 | A populated TestInfo namedtuple if found, else None. |
| 380 | """ |
| 381 | module_name, package = module_package.split(':') |
| 382 | module_info = self.find_test_by_module_name(module_name) |
| 383 | if not module_info: |
| 384 | return None |
| 385 | return self.find_test_by_package_name( |
| 386 | package, module_info.test_name, |
| 387 | module_info.data.get(constants.TI_REL_CONFIG)) |
| 388 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 389 | def find_test_by_path(self, path): |
| 390 | """Find the first test info matching the given path. |
| 391 | |
| 392 | Strategy: |
| 393 | path_to_java_file --> Resolve to CLASS |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 394 | path_to_cc_file --> Resolve to CC CLASS |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 395 | path_to_module_file -> Resolve to MODULE |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 396 | path_to_module_dir -> Resolve to MODULE |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 397 | path_to_dir_with_class_files--> Resolve to PACKAGE |
| 398 | path_to_any_other_dir --> Resolve as MODULE |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 399 | |
| 400 | Args: |
| 401 | path: A string of the test's path. |
| 402 | |
| 403 | Returns: |
| 404 | A populated TestInfo namedtuple if test found, else None |
| 405 | """ |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 406 | logging.debug('Finding test by path: %s', path) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 407 | path, methods = test_finder_utils.split_methods(path) |
| 408 | # TODO: See if this can be generalized and shared with methods above |
| 409 | # create absolute path from cwd and remove symbolic links |
| 410 | path = os.path.realpath(path) |
| 411 | if not os.path.exists(path): |
| 412 | return None |
| 413 | dir_path, file_name = test_finder_utils.get_dir_path_and_filename(path) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 414 | # Module/Class |
Simran Basi | 0da58b1 | 2018-02-28 17:47:30 -0800 | [diff] [blame] | 415 | rel_module_dir = test_finder_utils.find_parent_module_dir( |
| 416 | self.root_dir, dir_path, self.module_info) |
| 417 | if not rel_module_dir: |
| 418 | return None |
Simran Basi | 1f3c9c8 | 2018-08-20 17:08:00 -0700 | [diff] [blame] | 419 | module_name = self._determine_testable_module(rel_module_dir) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 420 | rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 421 | # The real test config might be record in module-info. |
| 422 | rel_config = self._get_module_test_config(module_name, rel_config=rel_config) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 423 | data = {constants.TI_REL_CONFIG: rel_config, |
| 424 | constants.TI_FILTER: frozenset()} |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 425 | # Path is to java file. |
yelinhsieh | e4580a5 | 2018-08-16 15:51:54 +0800 | [diff] [blame] | 426 | if file_name and _JAVA_EXT_RE.match(file_name): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 427 | full_class_name = test_finder_utils.get_fully_qualified_class_name( |
| 428 | path) |
| 429 | data[constants.TI_FILTER] = frozenset( |
| 430 | [test_info.TestFilter(full_class_name, methods)]) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 431 | # Path is to cc file. |
| 432 | elif file_name and _CC_EXT_RE.match(file_name): |
| 433 | if not test_finder_utils.has_cc_class(path): |
nelsonli | c4a7145 | 2018-09-13 14:10:30 +0800 | [diff] [blame] | 434 | raise atest_error.MissingCCTestCaseError( |
| 435 | "Can't find CC class in %s" % path) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 436 | if methods: |
| 437 | data[constants.TI_FILTER] = frozenset( |
| 438 | [test_info.TestFilter(test_finder_utils.get_cc_filter( |
| 439 | '*', methods), frozenset())]) |
| 440 | # Path to non-module dir, treat as package. |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 441 | elif (not file_name and not self.module_info.is_auto_gen_test_config(module_name) |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 442 | and rel_module_dir != os.path.relpath(path, self.root_dir)): |
| 443 | dir_items = [os.path.join(path, f) for f in os.listdir(path)] |
| 444 | for dir_item in dir_items: |
yelinhsieh | e4580a5 | 2018-08-16 15:51:54 +0800 | [diff] [blame] | 445 | if _JAVA_EXT_RE.match(dir_item): |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 446 | package_name = test_finder_utils.get_package_name(dir_item) |
| 447 | if package_name: |
| 448 | # methods should be empty frozenset for package. |
| 449 | if methods: |
nelsonli | c4a7145 | 2018-09-13 14:10:30 +0800 | [diff] [blame] | 450 | raise atest_error.MethodWithoutClassError( |
| 451 | '%s: Method filtering requires class' |
| 452 | % str(methods)) |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 453 | data[constants.TI_FILTER] = frozenset( |
| 454 | [test_info.TestFilter(package_name, methods)]) |
| 455 | break |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 456 | return self._process_test_info(test_info.TestInfo( |
| 457 | test_name=module_name, |
| 458 | test_runner=self._TEST_RUNNER, |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 459 | build_targets=set(), |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 460 | data=data)) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 461 | |
| 462 | def find_test_by_cc_class_name(self, class_name, module_name=None, |
| 463 | rel_config=None): |
| 464 | """Find test files given a cc class name. |
| 465 | |
| 466 | If module_name and rel_config not given, test will be determined |
| 467 | by looking up the tree for files which has input class. |
| 468 | |
| 469 | Args: |
| 470 | class_name: A string of the test's class name. |
| 471 | module_name: Optional. A string of the module name to use. |
| 472 | rel_config: Optional. A string of module dir relative to repo root. |
| 473 | |
| 474 | Returns: |
| 475 | A populated TestInfo namedtuple if test found, else None. |
| 476 | """ |
| 477 | return self.find_test_by_class_name(class_name, module_name, |
| 478 | rel_config, is_native_test=True) |
Jim Tang | d740fa5 | 2019-03-14 16:07:08 +0800 | [diff] [blame^] | 479 | |
| 480 | def get_testable_modules_with_ld(self, user_input, ld_range=0): |
| 481 | """Calculate the edit distances of the input and testable modules. |
| 482 | |
| 483 | The user input will be calculated across all testable modules and |
| 484 | results in integers generated by Levenshtein Distance algorithm. |
| 485 | To increase the speed of the calculation, a bound can be applied to |
| 486 | this method to prevent from calculating every testable modules. |
| 487 | |
| 488 | Guessing from typos, e.g. atest atest_unitests, implies a tangible range |
| 489 | of length that Atest only needs to search within it, and the default of |
| 490 | the bound is 2. |
| 491 | |
| 492 | Guessing from keywords however, e.g. atest --search Camera, means that |
| 493 | the uncertainty of the module name is way higher, and Atest should walk |
| 494 | through all testable modules and return the highest possibilities. |
| 495 | |
| 496 | Args: |
| 497 | user_input: A string of the user input. |
| 498 | ld_range: An integer that range the searching scope. If the length of |
| 499 | user_input is 10, then Atest will calculate modules of which |
| 500 | length is between 8 and 12. 0 is equivalent to unlimited. |
| 501 | |
| 502 | Returns: |
| 503 | A List of LDs and possible module names. If the user_input is "fax", |
| 504 | the output will be like: |
| 505 | [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]] |
| 506 | |
| 507 | Which means the most lilely names of "fax" are fog and Fix(LD=2), |
| 508 | while Dickies is the most unlikely one(LD=7). |
| 509 | """ |
| 510 | atest_utils.colorful_print('\nSearching for similar module names using ' |
| 511 | 'fuzzy search...', constants.CYAN) |
| 512 | testable_modules = sorted(self.module_info.get_testable_modules(), key=len) |
| 513 | lower_bound = len(user_input) - ld_range |
| 514 | upper_bound = len(user_input) + ld_range |
| 515 | testable_modules_with_ld = [] |
| 516 | for module_name in testable_modules: |
| 517 | # Dispose those too short or too lengthy. |
| 518 | if ld_range != 0: |
| 519 | if len(module_name) < lower_bound: |
| 520 | continue |
| 521 | elif len(module_name) > upper_bound: |
| 522 | break |
| 523 | testable_modules_with_ld.append( |
| 524 | [test_finder_utils.get_levenshtein_distance( |
| 525 | user_input, module_name), module_name]) |
| 526 | return testable_modules_with_ld |
| 527 | |
| 528 | def get_fuzzy_searching_results(self, user_input): |
| 529 | """Give results which have no more than allowance of edit distances. |
| 530 | |
| 531 | Args: |
| 532 | user_input: the target module name for fuzzy searching. |
| 533 | |
| 534 | Return: |
| 535 | A list of guessed modules. |
| 536 | """ |
| 537 | modules_with_ld = self.get_testable_modules_with_ld(user_input, |
| 538 | ld_range=constants.LD_RANGE) |
| 539 | guessed_modules = [] |
| 540 | for _distance, _module in modules_with_ld: |
| 541 | if _distance <= abs(constants.LD_RANGE): |
| 542 | guessed_modules.append(_module) |
| 543 | return guessed_modules |