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""" 16Module Finder class. 17""" 18 19import logging 20import os 21import re 22 23# pylint: disable=import-error 24import atest_error 25import constants 26import test_info 27import test_finder_base 28import test_finder_utils 29from test_runners import atest_tf_test_runner 30from test_runners import robolectric_test_runner 31from 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 47class 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