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 | |
kellyhung | 218551d | 2019-04-08 18:20:33 +0800 | [diff] [blame] | 215 | def _get_test_info_filter(self, path, methods, module_name, **kwargs): |
| 216 | """Get test info filter. |
| 217 | |
| 218 | Args: |
| 219 | path: A string of the test's path. |
| 220 | methods: A set of method name strings. |
| 221 | module_name: A string of the module name. |
| 222 | rel_module_dir: Optional. A string of the module dir relative to |
| 223 | root. |
| 224 | class_name: Optional. A string of the class name. |
| 225 | is_native_test: Optional. A boolean variable of whether to search |
| 226 | for a native test or not. |
| 227 | |
| 228 | Returns: |
| 229 | A set of test info filter. |
| 230 | """ |
| 231 | _, file_name = test_finder_utils.get_dir_path_and_filename(path) |
| 232 | ti_filter = frozenset() |
| 233 | if kwargs.get('is_native_test', None): |
| 234 | ti_filter = frozenset([test_info.TestFilter( |
| 235 | test_finder_utils.get_cc_filter( |
| 236 | kwargs.get('class_name', '*'), methods), frozenset())]) |
| 237 | # Path to java file. |
| 238 | elif file_name and _JAVA_EXT_RE.match(file_name): |
| 239 | full_class_name = test_finder_utils.get_fully_qualified_class_name( |
| 240 | path) |
| 241 | ti_filter = frozenset( |
| 242 | [test_info.TestFilter(full_class_name, methods)]) |
| 243 | # Path to cc file. |
| 244 | elif file_name and _CC_EXT_RE.match(file_name): |
| 245 | if not test_finder_utils.has_cc_class(path): |
| 246 | raise atest_error.MissingCCTestCaseError( |
| 247 | "Can't find CC class in %s" % path) |
| 248 | if methods: |
| 249 | ti_filter = frozenset( |
| 250 | [test_info.TestFilter(test_finder_utils.get_cc_filter( |
| 251 | kwargs.get('class_name', '*'), methods), frozenset())]) |
| 252 | # Path to non-module dir, treat as package. |
| 253 | elif (not file_name |
| 254 | and not self.module_info.is_auto_gen_test_config(module_name) |
| 255 | and kwargs.get('rel_module_dir', None) != |
| 256 | os.path.relpath(path, self.root_dir)): |
| 257 | dir_items = [os.path.join(path, f) for f in os.listdir(path)] |
| 258 | for dir_item in dir_items: |
| 259 | if _JAVA_EXT_RE.match(dir_item): |
| 260 | package_name = test_finder_utils.get_package_name(dir_item) |
| 261 | if package_name: |
| 262 | # methods should be empty frozenset for package. |
| 263 | if methods: |
| 264 | raise atest_error.MethodWithoutClassError( |
| 265 | '%s: Method filtering requires class' |
| 266 | % str(methods)) |
| 267 | ti_filter = frozenset( |
| 268 | [test_info.TestFilter(package_name, methods)]) |
| 269 | break |
| 270 | return ti_filter |
| 271 | |
kellyhung | b1cc264 | 2019-04-08 21:32:43 +0800 | [diff] [blame] | 272 | def _get_rel_config(self, test_path): |
| 273 | """Get config file's relative path. |
| 274 | |
| 275 | Args: |
| 276 | test_path: A string of the test absolute path. |
| 277 | |
| 278 | Returns: |
| 279 | A string of config's relative path, else None. |
| 280 | """ |
| 281 | test_dir = os.path.dirname(test_path) |
| 282 | rel_module_dir = test_finder_utils.find_parent_module_dir( |
| 283 | self.root_dir, test_dir, self.module_info) |
| 284 | if rel_module_dir: |
| 285 | return os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
| 286 | return None |
| 287 | |
| 288 | def _get_test_info(self, test_path, rel_config, module_name, test_filter): |
| 289 | """Get test_info for test_path. |
| 290 | |
| 291 | Args: |
| 292 | test_path: A string of the test path. |
| 293 | rel_config: A string of rel path of config. |
| 294 | module_name: A string of the module name to use. |
| 295 | test_filter: A test info filter. |
| 296 | |
| 297 | Returns: |
| 298 | TestInfo namedtuple if found, else None. |
| 299 | """ |
| 300 | if not rel_config: |
| 301 | rel_config = self._get_rel_config(test_path) |
| 302 | if not rel_config: |
| 303 | return None |
| 304 | if not module_name: |
| 305 | module_name = self._determine_testable_module( |
| 306 | os.path.dirname(rel_config)) |
| 307 | # The real test config might be recorded in module-info. |
| 308 | rel_config = self._get_module_test_config(module_name, |
| 309 | rel_config=rel_config) |
| 310 | return self._process_test_info(test_info.TestInfo( |
| 311 | test_name=module_name, |
| 312 | test_runner=self._TEST_RUNNER, |
| 313 | build_targets=set(), |
| 314 | data={constants.TI_FILTER: test_filter, |
| 315 | constants.TI_REL_CONFIG: rel_config})) |
| 316 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 317 | def find_test_by_module_name(self, module_name): |
| 318 | """Find test for the given module name. |
| 319 | |
| 320 | Args: |
| 321 | module_name: A string of the test's module name. |
| 322 | |
| 323 | Returns: |
| 324 | A populated TestInfo namedtuple if found, else None. |
| 325 | """ |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 326 | mod_info = self.module_info.get_module_info(module_name) |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 327 | if self.module_info.is_testable_module(mod_info): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 328 | # path is a list with only 1 element. |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 329 | rel_config = os.path.join(mod_info['path'][0], |
| 330 | constants.MODULE_CONFIG) |
nelsonli | 3cac134 | 2018-08-21 15:33:45 +0800 | [diff] [blame] | 331 | 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] | 332 | return self._process_test_info(test_info.TestInfo( |
| 333 | test_name=module_name, |
| 334 | test_runner=self._TEST_RUNNER, |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 335 | build_targets=set(), |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 336 | data={constants.TI_REL_CONFIG: rel_config, |
| 337 | constants.TI_FILTER: frozenset()})) |
| 338 | return None |
| 339 | |
| 340 | def find_test_by_class_name(self, class_name, module_name=None, |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 341 | rel_config=None, is_native_test=False): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 342 | """Find test files given a class name. |
| 343 | |
| 344 | If module_name and rel_config not given it will calculate it determine |
| 345 | it by looking up the tree from the class file. |
| 346 | |
| 347 | Args: |
| 348 | class_name: A string of the test's class name. |
| 349 | module_name: Optional. A string of the module name to use. |
| 350 | rel_config: Optional. A string of module dir relative to repo root. |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 351 | is_native_test: A boolean variable of whether to search for a |
| 352 | native test or not. |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 353 | |
| 354 | Returns: |
| 355 | A populated TestInfo namedtuple if test found, else None. |
| 356 | """ |
| 357 | class_name, methods = test_finder_utils.split_methods(class_name) |
| 358 | if rel_config: |
| 359 | search_dir = os.path.join(self.root_dir, |
| 360 | os.path.dirname(rel_config)) |
| 361 | else: |
| 362 | search_dir = self.root_dir |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 363 | test_path = test_finder_utils.find_class_file(search_dir, class_name, |
| 364 | is_native_test) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 365 | if not test_path and rel_config: |
| 366 | logging.info('Did not find class (%s) under module path (%s), ' |
| 367 | 'researching from repo root.', class_name, rel_config) |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 368 | test_path = test_finder_utils.find_class_file(self.root_dir, |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 369 | class_name, |
| 370 | is_native_test) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 371 | if not test_path: |
| 372 | return None |
kellyhung | 218551d | 2019-04-08 18:20:33 +0800 | [diff] [blame] | 373 | test_filter = self._get_test_info_filter( |
| 374 | test_path, methods, module_name, class_name=class_name, |
| 375 | is_native_test=is_native_test) |
kellyhung | b1cc264 | 2019-04-08 21:32:43 +0800 | [diff] [blame] | 376 | tinfo = self._get_test_info(test_path, rel_config, module_name, |
| 377 | test_filter) |
| 378 | return tinfo |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 379 | |
| 380 | def find_test_by_module_and_class(self, module_class): |
| 381 | """Find the test info given a MODULE:CLASS string. |
| 382 | |
| 383 | Args: |
| 384 | module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD. |
| 385 | |
| 386 | Returns: |
| 387 | A populated TestInfo namedtuple if found, else None. |
| 388 | """ |
| 389 | if ':' not in module_class: |
| 390 | return None |
| 391 | module_name, class_name = module_class.split(':') |
| 392 | module_info = self.find_test_by_module_name(module_name) |
| 393 | if not module_info: |
| 394 | return None |
yangbill | 57739f8 | 2019-03-28 18:25:43 +0800 | [diff] [blame] | 395 | # If the target module is NATIVE_TEST, search CC classes only. |
| 396 | find_result = None |
| 397 | if not self.module_info.is_native_test(module_name): |
| 398 | # Find by java class. |
| 399 | find_result = self.find_test_by_class_name( |
| 400 | class_name, module_info.test_name, |
| 401 | module_info.data.get(constants.TI_REL_CONFIG)) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 402 | # Find by cc class. |
| 403 | if not find_result: |
| 404 | find_result = self.find_test_by_cc_class_name( |
| 405 | class_name, module_info.test_name, |
| 406 | module_info.data.get(constants.TI_REL_CONFIG)) |
| 407 | return find_result |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 408 | |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 409 | def find_test_by_package_name(self, package, module_name=None, |
| 410 | rel_config=None): |
| 411 | """Find the test info given a PACKAGE string. |
| 412 | |
| 413 | Args: |
| 414 | package: A string of the package name. |
| 415 | module_name: Optional. A string of the module name. |
| 416 | ref_config: Optional. A string of rel path of config. |
| 417 | |
| 418 | Returns: |
| 419 | A populated TestInfo namedtuple if found, else None. |
| 420 | """ |
| 421 | _, methods = test_finder_utils.split_methods(package) |
| 422 | if methods: |
nelsonli | c4a7145 | 2018-09-13 14:10:30 +0800 | [diff] [blame] | 423 | raise atest_error.MethodWithoutClassError('%s: Method filtering ' |
| 424 | 'requires class' % ( |
| 425 | methods)) |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 426 | # Confirm that packages exists and get user input for multiples. |
| 427 | if rel_config: |
| 428 | search_dir = os.path.join(self.root_dir, |
| 429 | os.path.dirname(rel_config)) |
| 430 | else: |
| 431 | search_dir = self.root_dir |
| 432 | package_path = test_finder_utils.run_find_cmd( |
| 433 | test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, |
| 434 | package.replace('.', '/')) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 435 | # Package path will be the full path to the dir represented by package. |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 436 | if not package_path: |
| 437 | return None |
| 438 | test_filter = frozenset([test_info.TestFilter(package, frozenset())]) |
kellyhung | b1cc264 | 2019-04-08 21:32:43 +0800 | [diff] [blame] | 439 | tinfo = self._get_test_info(package_path, rel_config, module_name, |
| 440 | test_filter) |
| 441 | return tinfo |
mikehoran | 6740a41 | 2018-02-20 15:04:16 -0800 | [diff] [blame] | 442 | |
| 443 | def find_test_by_module_and_package(self, module_package): |
| 444 | """Find the test info given a MODULE:PACKAGE string. |
| 445 | |
| 446 | Args: |
| 447 | module_package: A string of form MODULE:PACKAGE |
| 448 | |
| 449 | Returns: |
| 450 | A populated TestInfo namedtuple if found, else None. |
| 451 | """ |
| 452 | module_name, package = module_package.split(':') |
| 453 | module_info = self.find_test_by_module_name(module_name) |
| 454 | if not module_info: |
| 455 | return None |
| 456 | return self.find_test_by_package_name( |
| 457 | package, module_info.test_name, |
| 458 | module_info.data.get(constants.TI_REL_CONFIG)) |
| 459 | |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 460 | def find_test_by_path(self, path): |
| 461 | """Find the first test info matching the given path. |
| 462 | |
| 463 | Strategy: |
| 464 | path_to_java_file --> Resolve to CLASS |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 465 | path_to_cc_file --> Resolve to CC CLASS |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 466 | path_to_module_file -> Resolve to MODULE |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 467 | path_to_module_dir -> Resolve to MODULE |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 468 | path_to_dir_with_class_files--> Resolve to PACKAGE |
| 469 | path_to_any_other_dir --> Resolve as MODULE |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 470 | |
| 471 | Args: |
| 472 | path: A string of the test's path. |
| 473 | |
| 474 | Returns: |
| 475 | A populated TestInfo namedtuple if test found, else None |
| 476 | """ |
mikehoran | 8bf6d08 | 2018-02-26 16:22:06 -0800 | [diff] [blame] | 477 | logging.debug('Finding test by path: %s', path) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 478 | path, methods = test_finder_utils.split_methods(path) |
| 479 | # TODO: See if this can be generalized and shared with methods above |
| 480 | # create absolute path from cwd and remove symbolic links |
| 481 | path = os.path.realpath(path) |
| 482 | if not os.path.exists(path): |
| 483 | return None |
kellyhung | 218551d | 2019-04-08 18:20:33 +0800 | [diff] [blame] | 484 | dir_path, _ = test_finder_utils.get_dir_path_and_filename(path) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 485 | # Module/Class |
Simran Basi | 0da58b1 | 2018-02-28 17:47:30 -0800 | [diff] [blame] | 486 | rel_module_dir = test_finder_utils.find_parent_module_dir( |
| 487 | self.root_dir, dir_path, self.module_info) |
| 488 | if not rel_module_dir: |
| 489 | return None |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 490 | rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
kellyhung | b1cc264 | 2019-04-08 21:32:43 +0800 | [diff] [blame] | 491 | test_filter = self._get_test_info_filter(path, methods, None, |
| 492 | rel_module_dir=rel_module_dir) |
| 493 | return self._get_test_info(path, rel_config, None, test_filter) |
kellyhung | f4b5947 | 2018-05-10 17:00:10 +0800 | [diff] [blame] | 494 | |
| 495 | def find_test_by_cc_class_name(self, class_name, module_name=None, |
| 496 | rel_config=None): |
| 497 | """Find test files given a cc class name. |
| 498 | |
| 499 | If module_name and rel_config not given, test will be determined |
| 500 | by looking up the tree for files which has input class. |
| 501 | |
| 502 | Args: |
| 503 | class_name: A string of the test's class name. |
| 504 | module_name: Optional. A string of the module name to use. |
| 505 | rel_config: Optional. A string of module dir relative to repo root. |
| 506 | |
| 507 | Returns: |
| 508 | A populated TestInfo namedtuple if test found, else None. |
| 509 | """ |
Dan Shi | c095efc | 2019-04-18 16:05:52 -0700 | [diff] [blame] | 510 | # Check if class_name is prepended with file name. If so, trim the |
| 511 | # prefix and keep only the class_name. |
| 512 | if '.' in class_name: |
| 513 | # Assume the class name has a format of file_name.class_name |
| 514 | class_name = class_name[class_name.rindex('.')+1:] |
| 515 | logging.info('Search with updated class name: %s', class_name) |
| 516 | return self.find_test_by_class_name( |
| 517 | class_name, module_name, rel_config, is_native_test=True) |
Jim Tang | d740fa5 | 2019-03-14 16:07:08 +0800 | [diff] [blame] | 518 | |
| 519 | def get_testable_modules_with_ld(self, user_input, ld_range=0): |
| 520 | """Calculate the edit distances of the input and testable modules. |
| 521 | |
| 522 | The user input will be calculated across all testable modules and |
| 523 | results in integers generated by Levenshtein Distance algorithm. |
| 524 | To increase the speed of the calculation, a bound can be applied to |
| 525 | this method to prevent from calculating every testable modules. |
| 526 | |
| 527 | Guessing from typos, e.g. atest atest_unitests, implies a tangible range |
| 528 | of length that Atest only needs to search within it, and the default of |
| 529 | the bound is 2. |
| 530 | |
| 531 | Guessing from keywords however, e.g. atest --search Camera, means that |
| 532 | the uncertainty of the module name is way higher, and Atest should walk |
| 533 | through all testable modules and return the highest possibilities. |
| 534 | |
| 535 | Args: |
| 536 | user_input: A string of the user input. |
| 537 | ld_range: An integer that range the searching scope. If the length of |
| 538 | user_input is 10, then Atest will calculate modules of which |
| 539 | length is between 8 and 12. 0 is equivalent to unlimited. |
| 540 | |
| 541 | Returns: |
| 542 | A List of LDs and possible module names. If the user_input is "fax", |
| 543 | the output will be like: |
| 544 | [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]] |
| 545 | |
| 546 | Which means the most lilely names of "fax" are fog and Fix(LD=2), |
| 547 | while Dickies is the most unlikely one(LD=7). |
| 548 | """ |
| 549 | atest_utils.colorful_print('\nSearching for similar module names using ' |
| 550 | 'fuzzy search...', constants.CYAN) |
| 551 | testable_modules = sorted(self.module_info.get_testable_modules(), key=len) |
| 552 | lower_bound = len(user_input) - ld_range |
| 553 | upper_bound = len(user_input) + ld_range |
| 554 | testable_modules_with_ld = [] |
| 555 | for module_name in testable_modules: |
| 556 | # Dispose those too short or too lengthy. |
| 557 | if ld_range != 0: |
| 558 | if len(module_name) < lower_bound: |
| 559 | continue |
| 560 | elif len(module_name) > upper_bound: |
| 561 | break |
| 562 | testable_modules_with_ld.append( |
| 563 | [test_finder_utils.get_levenshtein_distance( |
| 564 | user_input, module_name), module_name]) |
| 565 | return testable_modules_with_ld |
| 566 | |
| 567 | def get_fuzzy_searching_results(self, user_input): |
| 568 | """Give results which have no more than allowance of edit distances. |
| 569 | |
| 570 | Args: |
| 571 | user_input: the target module name for fuzzy searching. |
| 572 | |
| 573 | Return: |
| 574 | A list of guessed modules. |
| 575 | """ |
| 576 | modules_with_ld = self.get_testable_modules_with_ld(user_input, |
| 577 | ld_range=constants.LD_RANGE) |
| 578 | guessed_modules = [] |
| 579 | for _distance, _module in modules_with_ld: |
| 580 | if _distance <= abs(constants.LD_RANGE): |
| 581 | guessed_modules.append(_module) |
| 582 | return guessed_modules |