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"""Utils for finder classes.""" 16 17# pylint: disable=too-many-lines 18 19from __future__ import print_function 20 21from contextlib import contextmanager 22from enum import Enum, unique 23import logging 24import os 25from pathlib import Path 26import pickle 27import re 28import shutil 29import subprocess 30import sys 31import tempfile 32import time 33from typing import Any, Dict, Iterable, List, Set, Tuple 34import xml.etree.ElementTree as ET 35 36from atest import atest_error 37from atest import atest_utils 38from atest import constants 39from atest import module_info 40from atest.atest_enum import DetectType, ExitCode 41from atest.metrics import metrics, metrics_utils 42from atest.test_finders import test_filter_utils 43 44# Helps find apk files listed in a test config (AndroidTest.xml) file. 45# Matches "filename.apk" in <option name="foo", value="filename.apk" /> 46# We want to make sure we don't grab apks with paths in their name since we 47# assume the apk name is the build target. 48_APK_RE = re.compile(r'^[^/]+\.apk$', re.I) 49 50 51# Group that matches java/kt method. 52_JAVA_METHODS_RE = r'.*\s+(fun|void)\s+(?P<method>\w+)\(' 53# Matches install paths in module_info to install location(host or device). 54_HOST_PATH_RE = re.compile(r'.*\/host\/.*', re.I) 55_DEVICE_PATH_RE = re.compile(r'.*\/target\/.*', re.I) 56# RE for Java/Kt parent classes: 57# Java: class A extends B {...} 58# Kotlin: class A : B (...) 59_PARENT_CLS_RE = re.compile( 60 r'.*class\s+\w+\s+(?:extends|:)\s+' r'(?P<parent>[\w\.]+)\s*(?:\{|\()' 61) 62_CC_GREP_RE = r'^\s*(TYPED_TEST(_P)*|TEST(_F|_P)*)\s*\({1},' 63 64 65@unique 66class TestReferenceType(Enum): 67 """An Enum class that stores the ways of finding a reference.""" 68 69 # Name of a java/kotlin class, usually file is named the same 70 # (HostTest lives in HostTest.java or HostTest.kt) 71 CLASS = ( 72 constants.CLASS_INDEX, 73 r"find {0} -type f| egrep '.*/{1}\.(kt|java)$' || true", 74 ) 75 # Like CLASS but also contains the package in front like 76 # com.android.tradefed.testtype.HostTest. 77 QUALIFIED_CLASS = ( 78 constants.QCLASS_INDEX, 79 r"find {0} -type f | egrep '.*{1}\.(kt|java)$' || true", 80 ) 81 # Name of a Java package. 82 PACKAGE = ( 83 constants.PACKAGE_INDEX, 84 r"find {0} -wholename '*{1}' -type d -print", 85 ) 86 # XML file name in one of the 4 integration config directories. 87 INTEGRATION = ( 88 constants.INT_INDEX, 89 r"find {0} -wholename '*/{1}\.xml' -print", 90 ) 91 # Name of a cc/cpp class. 92 CC_CLASS = ( 93 constants.CC_CLASS_INDEX, 94 ( 95 r"find {0} -type f -print | egrep -i '/*test.*\.(cc|cpp)$'" 96 f"| xargs -P0 egrep -sH '{_CC_GREP_RE}' || true" 97 ), 98 ) 99 100 def __init__(self, index_file, find_command): 101 self.index_file = index_file 102 self.find_command = find_command 103 104 105# XML parsing related constants. 106_COMPATIBILITY_PACKAGE_PREFIX = 'com.android.compatibility' 107_XML_PUSH_DELIM = '->' 108_APK_SUFFIX = '.apk' 109DALVIK_TEST_RUNNER_CLASS = 'com.android.compatibility.testtype.DalvikTest' 110LIBCORE_TEST_RUNNER_CLASS = 'com.android.compatibility.testtype.LibcoreTest' 111DALVIK_TESTRUNNER_JAR_CLASSES = [ 112 DALVIK_TEST_RUNNER_CLASS, 113 LIBCORE_TEST_RUNNER_CLASS, 114] 115DALVIK_DEVICE_RUNNER_JAR = 'cts-dalvik-device-test-runner' 116DALVIK_HOST_RUNNER_JAR = 'cts-dalvik-host-test-runner' 117DALVIK_TEST_DEPS = { 118 DALVIK_DEVICE_RUNNER_JAR, 119 DALVIK_HOST_RUNNER_JAR, 120 constants.CTS_JAR, 121} 122# Setup script for device perf tests. 123_PERF_SETUP_LABEL = 'perf-setup.sh' 124_PERF_SETUP_TARGET = 'perf-setup' 125 126# XML tags. 127_XML_NAME = 'name' 128_XML_VALUE = 'value' 129 130# VTS xml parsing constants. 131_VTS_TEST_MODULE = 'test-module-name' 132_VTS_MODULE = 'module-name' 133_VTS_BINARY_SRC = 'binary-test-source' 134_VTS_PUSH_GROUP = 'push-group' 135_VTS_PUSH = 'push' 136_VTS_BINARY_SRC_DELIM = '::' 137_VTS_PUSH_DIR = os.path.join( 138 os.environ.get(constants.ANDROID_BUILD_TOP, ''), 139 'test', 140 'vts', 141 'tools', 142 'vts-tradefed', 143 'res', 144 'push_groups', 145) 146_VTS_PUSH_SUFFIX = '.push' 147_VTS_BITNESS = 'append-bitness' 148_VTS_BITNESS_TRUE = 'true' 149_VTS_BITNESS_32 = '32' 150_VTS_BITNESS_64 = '64' 151_VTS_TEST_FILE = 'test-file-name' 152_VTS_APK = 'apk' 153# Matches 'DATA/target' in '_32bit::DATA/target' 154_VTS_BINARY_SRC_DELIM_RE = re.compile(r'.*::(?P<target>.*)$') 155_VTS_OUT_DATA_APP_PATH = 'DATA/app' 156 157# Auxiliary options for multiple test selector 158_ALL_OPTION = 'A' 159_CANCEL_OPTION = 'C' 160 161 162def has_cc_class(test_path): 163 """Find out if there is any test case in the cc file. 164 165 Args: 166 test_path: A string of absolute path to the cc file. 167 168 Returns: 169 Boolean: has cc class in test_path or not. 170 """ 171 with open_cc(test_path) as class_file: 172 content = class_file.read() 173 if re.findall(test_filter_utils.CC_CLASS_METHOD_RE, content): 174 return True 175 if re.findall(test_filter_utils.CC_FLAG_CLASS_METHOD_RE, content): 176 return True 177 if re.findall(test_filter_utils.CC_PARAM_CLASS_RE, content): 178 return True 179 if re.findall(test_filter_utils.TYPE_CC_CLASS_RE, content): 180 return True 181 return False 182 183 184def get_parent_cls_name(file_name): 185 """Parse the parent class name from a java/kt file. 186 187 Args: 188 file_name: A string of the absolute path to the javai/kt file. 189 190 Returns: 191 A string of the parent class name or None 192 """ 193 with open(file_name) as data: 194 for line in data: 195 match = _PARENT_CLS_RE.match(line) 196 if match: 197 return match.group('parent') 198 199 200def get_java_parent_paths(test_path): 201 """Find out the paths of parent classes, including itself. 202 203 Args: 204 test_path: A string of absolute path to the test file. 205 206 Returns: 207 A set of test paths. 208 """ 209 all_parent_test_paths = set([test_path]) 210 parent = get_parent_cls_name(test_path) 211 if not parent: 212 return all_parent_test_paths 213 # Remove <Generics> if any. 214 parent_cls = re.sub(r'\<\w+\>', '', parent) 215 package = test_filter_utils.get_package_name(test_path) 216 # Use Fully Qualified Class Name for searching precisely. 217 # package org.gnome; 218 # public class Foo extends com.android.Boo -> com.android.Boo 219 # public class Foo extends Boo -> org.gnome.Boo 220 if '.' in parent_cls: 221 parent_fqcn = parent_cls 222 else: 223 parent_fqcn = package + '.' + parent_cls 224 parent_test_paths = run_find_cmd( 225 TestReferenceType.QUALIFIED_CLASS, 226 os.environ.get(constants.ANDROID_BUILD_TOP), 227 parent_fqcn, 228 ) 229 # Recursively search parent classes until the class is not found. 230 if parent_test_paths: 231 for parent_test_path in parent_test_paths: 232 all_parent_test_paths |= get_java_parent_paths(parent_test_path) 233 return all_parent_test_paths 234 235 236def has_method_in_file(test_path, methods): 237 """Find out if every method can be found in the file. 238 239 Note: This method doesn't handle if method is in comment sections. 240 241 Args: 242 test_path: A string of absolute path to the test file. 243 methods: A set of method names. 244 245 Returns: 246 Boolean: there is at least one method in test_path. 247 """ 248 if not os.path.isfile(test_path): 249 return False 250 all_methods = set() 251 if constants.JAVA_EXT_RE.match(test_path): 252 # omit parameterized pattern: method[0] 253 _methods = set(re.sub(r'\[\S+\]', '', x) for x in methods) 254 # Return True when every method is in the same Java file. 255 if _methods.issubset(get_java_methods(test_path)): 256 return True 257 # Otherwise, search itself and all the parent classes respectively 258 # to get all test names. 259 parent_test_paths = get_java_parent_paths(test_path) 260 logging.debug('Will search methods %s in %s\n', _methods, parent_test_paths) 261 for path in parent_test_paths: 262 all_methods |= get_java_methods(path) 263 if _methods.issubset(all_methods): 264 return True 265 # If cannot find all methods, override the test_path for debugging. 266 test_path = parent_test_paths 267 elif constants.CC_EXT_RE.match(test_path): 268 # omit parameterized pattern: method/argument 269 _methods = set(re.sub(r'\/.*', '', x) for x in methods) 270 class_info = get_cc_class_info(test_path) 271 for info in class_info.values(): 272 all_methods |= info.get('methods') 273 if _methods.issubset(all_methods): 274 return True 275 missing_methods = _methods - all_methods 276 logging.debug( 277 'Cannot find methods %s in %s', 278 atest_utils.mark_red(','.join(missing_methods)), 279 test_path, 280 ) 281 return False 282 283 284def extract_test_path(output, methods=None): 285 """Extract the test path from the output of a unix 'find' command. 286 287 Example of find output for CLASS find cmd: 288 /<some_root>/cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java 289 290 Args: 291 output: A string or list output of a unix 'find' command. 292 methods: A set of method names. 293 294 Returns: 295 A list of the test paths or None if output is '' or None. 296 """ 297 if not output: 298 return None 299 verified_tests = set() 300 if isinstance(output, str): 301 output = output.splitlines() 302 for test in output: 303 match_obj = constants.CC_OUTPUT_RE.match(test) 304 # Legacy "find" cc output (with TEST_P() syntax): 305 if match_obj: 306 fpath = match_obj.group('file_path') 307 if not methods or match_obj.group('method_name') in methods: 308 verified_tests.add(fpath) 309 # "locate" output path for both java/cc. 310 elif not methods or has_method_in_file(test, methods): 311 verified_tests.add(test) 312 return extract_selected_tests(sorted(list(verified_tests))) 313 314 315def extract_selected_tests(tests: Iterable, default_all=False) -> List[str]: 316 """Extract the test path from the tests. 317 318 Return the test to run from tests. If more than one option, prompt the user 319 to select multiple ones. Supporting formats: 320 - A string for the auxiliary menu: A for All, C for Cancel 321 - An integer. E.g. 0 322 - Comma-separated integers. E.g. 1,3,5 323 - A range of integers denoted by the starting integer separated from 324 the end integer by a dash, '-'. E.g. 1-3 325 326 Args: 327 tests: A string list which contains multiple test paths. 328 329 Returns: 330 A string list of paths. 331 """ 332 tests = sorted(list(tests)) 333 count = len(tests) 334 if default_all or count <= 1: 335 return tests if count else None 336 337 extracted_tests = set() 338 auxiliary_menu = [f'{_ALL_OPTION}: All', f'{_CANCEL_OPTION}: Cancel'] 339 numbered_list = ['%s: %s' % (i, t) for i, t in enumerate(tests)] 340 print( 341 'Multiple tests found:\n{0}'.format( 342 '\n'.join(auxiliary_menu + numbered_list) 343 ) 344 ) 345 346 start_prompt = time.time() 347 answer = get_multiple_selection_answer() 348 if _ALL_OPTION in answer.upper(): 349 extracted_tests = tests 350 elif _CANCEL_OPTION in answer.upper(): 351 atest_utils.colorful_print('Abort selection.', constants.RED) 352 sys.exit(0) 353 else: 354 extracted_tests = { 355 tests[index] 356 for index in get_selected_indices(answer, limit=len(numbered_list) - 1) 357 } 358 metrics.LocalDetectEvent( 359 detect_type=DetectType.INTERACTIVE_SELECTION, 360 result=int(time.time() - start_prompt), 361 ) 362 363 return list(extracted_tests) 364 365 366def get_multiple_selection_answer() -> str: 367 """Get the answer from the user input.""" 368 try: 369 return input( 370 'Please select an option.' 371 '\n(multiple selection is supported, ' 372 "e.g. '1' or '0,1' or '0-2'): " 373 ) 374 except KeyboardInterrupt: 375 return _CANCEL_OPTION 376 377 378def get_selected_indices(string: str, limit: int = None) -> Set[int]: 379 """Method which flattens and dedups the given string to a set of integer. 380 381 This method is also capable to convert '5-2' to {2,3,4,5}. e.g. 382 '0, 2-5, 5-3' -> {0, 2, 3, 4, 5} 383 384 If the given string contains non-numerical string, returns an empty set. 385 386 Args: 387 string: a given string, e.g. '0, 2-5' 388 limit: an integer that every parsed number cannot exceed. 389 390 Returns: 391 A set of integer. If one of the parsed number exceeds the limit, or 392 invalid string such as '2-5-7', returns an empty set instead. 393 """ 394 selections = set() 395 try: 396 for num_str in re.sub(r'\s', '', string).split(','): 397 ranged_num_str = num_str.split('-') 398 if len(ranged_num_str) == 2: 399 start = min([int(n) for n in ranged_num_str]) 400 end = max([int(n) for n in ranged_num_str]) 401 selections |= {n for n in range(start, end + 1)} 402 elif len(ranged_num_str) == 1: 403 selections.add(int(num_str)) 404 if limit and any(n for n in selections if n > limit): 405 raise ValueError 406 except ( 407 ValueError, 408 IndexError, 409 AttributeError, 410 TypeError, 411 KeyboardInterrupt, 412 ) as err: 413 logging.debug('%s', err) 414 atest_utils.colorful_print('Invalid input detected.', constants.RED) 415 return set() 416 417 return selections 418 419 420def run_find_cmd(ref_type, search_dir, target, methods=None): 421 """Find a path to a target given a search dir and a target name. 422 423 Args: 424 ref_type: An Enum of the reference type. 425 search_dir: A string of the dirpath to search in. 426 target: A string of what you're trying to find. 427 methods: A set of method names. 428 429 Return: 430 A list of the path to the target. 431 If the search_dir is inexistent, None will be returned. 432 """ 433 if not os.path.isdir(search_dir): 434 logging.debug("'%s' does not exist!", search_dir) 435 return None 436 ref_name = ref_type.name 437 index_file = ref_type.index_file 438 start = time.time() 439 if os.path.isfile(index_file): 440 _dict, out = {}, None 441 with open(index_file, 'rb') as index: 442 try: 443 _dict = pickle.load(index, encoding='utf-8') 444 except ( 445 UnicodeDecodeError, 446 TypeError, 447 IOError, 448 EOFError, 449 AttributeError, 450 pickle.UnpicklingError, 451 ) as err: 452 logging.debug('Error occurs while loading %s: %s', index_file, err) 453 metrics_utils.handle_exc_and_send_exit_event( 454 constants.ACCESS_CACHE_FAILURE 455 ) 456 os.remove(index_file) 457 if _dict.get(target): 458 out = [path for path in _dict.get(target) if search_dir in path] 459 logging.debug('Found %s in %s', target, out) 460 else: 461 if '.' in target: 462 target = target.replace('.', '/') 463 find_cmd = ref_type.find_command.format(search_dir, target) 464 logging.debug('Executing %s find cmd: %s', ref_name, find_cmd) 465 out = subprocess.check_output(find_cmd, shell=True) 466 if isinstance(out, bytes): 467 out = out.decode() 468 logging.debug('%s find cmd out: %s', ref_name, out) 469 logging.debug('%s find completed in %ss', ref_name, time.time() - start) 470 return extract_test_path(out, methods) 471 472 473def find_class_file(search_dir, class_name, is_native_test=False, methods=None): 474 """Find a path to a class file given a search dir and a class name. 475 476 Args: 477 search_dir: A string of the dirpath to search in. 478 class_name: A string of the class to search for. 479 is_native_test: A boolean variable of whether to search for a native test 480 or not. 481 methods: A set of method names. 482 483 Return: 484 A list of the path to the java/cc file. 485 """ 486 if is_native_test: 487 ref_type = TestReferenceType.CC_CLASS 488 elif '.' in class_name: 489 ref_type = TestReferenceType.QUALIFIED_CLASS 490 else: 491 ref_type = TestReferenceType.CLASS 492 return run_find_cmd(ref_type, search_dir, class_name, methods) 493 494 495def is_equal_or_sub_dir(sub_dir, parent_dir): 496 """Return True sub_dir is sub dir or equal to parent_dir. 497 498 Args: 499 sub_dir: A string of the sub directory path. 500 parent_dir: A string of the parent directory path. 501 502 Returns: 503 A boolean of whether both are dirs and sub_dir is sub of parent_dir 504 or is equal to parent_dir. 505 """ 506 # avoid symlink issues with real path 507 parent_dir = os.path.realpath(parent_dir) 508 sub_dir = os.path.realpath(sub_dir) 509 if not os.path.isdir(sub_dir) or not os.path.isdir(parent_dir): 510 return False 511 return os.path.commonprefix([sub_dir, parent_dir]) == parent_dir 512 513 514def find_parent_module_dir(root_dir, start_dir, module_info): 515 """From current dir search up file tree until root dir for module dir. 516 517 Args: 518 root_dir: A string of the dir that is the parent of the start dir. 519 start_dir: A string of the dir to start searching up from. 520 module_info: ModuleInfo object containing module information from the 521 build system. 522 523 Returns: 524 A string of the module dir relative to root, None if no Module Dir 525 found. There may be multiple testable modules at this level. 526 527 Exceptions: 528 ValueError: Raised if cur_dir not dir or not subdir of root dir. 529 """ 530 if not is_equal_or_sub_dir(start_dir, root_dir): 531 raise ValueError('%s not in repo %s' % (start_dir, root_dir)) 532 auto_gen_dir = None 533 current_dir = start_dir 534 # Look for AndroidTest.xml config starting in the current dir up to the root 535 # dir. 536 while current_dir != root_dir: 537 # TODO (b/112904944) - migrate module_finder functions to here and 538 # reuse them. 539 rel_dir = os.path.relpath(current_dir, root_dir) 540 # Check if actual config file here but need to make sure that there 541 # exist module in module-info with the parent dir. 542 if os.path.isfile( 543 os.path.join(current_dir, constants.MODULE_CONFIG) 544 ) and module_info.get_module_names(current_dir): 545 return rel_dir 546 # Check module_info if auto_gen config or robo (non-config) here 547 for mod in module_info.path_to_module_info.get(rel_dir, []): 548 if module_info.is_legacy_robolectric_class(mod): 549 return rel_dir 550 for test_config in mod.get(constants.MODULE_TEST_CONFIG, []): 551 # If the test config doesn't exist until it was auto-generated 552 # in the build time(under <android_root>/out), atest still 553 # recognizes it testable. 554 if test_config: 555 return rel_dir 556 if mod.get('auto_test_config'): 557 auto_gen_dir = rel_dir 558 # Don't return for auto_gen, keep checking for real config, 559 # because common in cts for class in apk that's in hostside 560 # test setup. 561 current_dir = os.path.dirname(current_dir) 562 return auto_gen_dir 563 564 565def get_targets_from_xml(xml_file, module_info): 566 """Retrieve build targets from the given xml. 567 568 Just a helper func on top of get_targets_from_xml_root. 569 570 Args: 571 xml_file: abs path to xml file. 572 module_info: ModuleInfo class used to verify targets are valid modules. 573 574 Returns: 575 A set of build targets based on the signals found in the xml file. 576 """ 577 if not os.path.isfile(xml_file): 578 return set() 579 xml_root = ET.parse(xml_file).getroot() 580 return get_targets_from_xml_root(xml_root, module_info) 581 582 583def _get_apk_target(apk_target): 584 """Return the sanitized apk_target string from the xml. 585 586 The apk_target string can be of 2 forms: 587 - apk_target.apk 588 - apk_target.apk->/path/to/install/apk_target.apk 589 590 We want to return apk_target in both cases. 591 592 Args: 593 apk_target: String of target name to clean. 594 595 Returns: 596 String of apk_target to build. 597 """ 598 apk = apk_target.split(_XML_PUSH_DELIM, 1)[0].strip() 599 return apk[: -len(_APK_SUFFIX)] 600 601 602def _is_apk_target(name, value): 603 """Return True if XML option is an apk target. 604 605 We have some scenarios where an XML option can be an apk target: 606 - value is an apk file. 607 - name is a 'push' option where value holds the apk_file + other stuff. 608 609 Args: 610 name: String name of XML option. 611 value: String value of the XML option. 612 613 Returns: 614 True if it's an apk target we should build, False otherwise. 615 """ 616 if _APK_RE.match(value): 617 return True 618 if name == 'push' and value.endswith(_APK_SUFFIX): 619 return True 620 return False 621 622 623def get_targets_from_xml_root(xml_root, module_info): 624 """Retrieve build targets from the given xml root. 625 626 We're going to pull the following bits of info: 627 - Parse any .apk files listed in the config file. 628 - Parse option value for "test-module-name" (for vts10 tests). 629 - Look for the perf script. 630 631 Args: 632 module_info: ModuleInfo class used to verify targets are valid modules. 633 xml_root: ElementTree xml_root for us to look through. 634 635 Returns: 636 A set of build targets based on the signals found in the xml file. 637 """ 638 targets = set() 639 option_tags = xml_root.findall('.//option') 640 for tag in option_tags: 641 target_to_add = None 642 name = tag.attrib.get(_XML_NAME, '').strip() 643 value = tag.attrib.get(_XML_VALUE, '').strip() 644 if _is_apk_target(name, value): 645 target_to_add = _get_apk_target(value) 646 elif _PERF_SETUP_LABEL in value: 647 target_to_add = _PERF_SETUP_TARGET 648 649 # Let's make sure we can actually build the target. 650 if target_to_add and module_info.is_module(target_to_add): 651 targets.add(target_to_add) 652 elif target_to_add: 653 logging.debug( 654 'Build target (%s) not present in module info, skipping build', 655 target_to_add, 656 ) 657 658 # TODO (b/70813166): Remove this lookup once all runtime dependencies 659 # can be listed as a build dependencies or are in the base test harness. 660 nodes_with_class = xml_root.findall('.//*[@class]') 661 for class_attr in nodes_with_class: 662 fqcn = class_attr.attrib['class'].strip() 663 if fqcn.startswith(_COMPATIBILITY_PACKAGE_PREFIX): 664 targets.add(constants.CTS_JAR) 665 if fqcn in DALVIK_TESTRUNNER_JAR_CLASSES: 666 for dalvik_dep in DALVIK_TEST_DEPS: 667 if module_info.is_module(dalvik_dep): 668 targets.add(dalvik_dep) 669 logging.debug('Targets found in config file: %s', targets) 670 return targets 671 672 673def _get_vts_push_group_targets(push_file, rel_out_dir): 674 """Retrieve vts10 push group build targets. 675 676 A push group file is a file that list out test dependencies and other push 677 group files. Go through the push file and gather all the test deps we need. 678 679 Args: 680 push_file: Name of the push file in the VTS 681 rel_out_dir: Abs path to the out dir to help create vts10 build targets. 682 683 Returns: 684 Set of string which represent build targets. 685 """ 686 targets = set() 687 full_push_file_path = os.path.join(_VTS_PUSH_DIR, push_file) 688 # pylint: disable=invalid-name 689 with open(full_push_file_path) as f: 690 for line in f: 691 target = line.strip() 692 # Skip empty lines. 693 if not target: 694 continue 695 696 # This is a push file, get the targets from it. 697 if target.endswith(_VTS_PUSH_SUFFIX): 698 targets |= _get_vts_push_group_targets(line.strip(), rel_out_dir) 699 continue 700 sanitized_target = target.split(_XML_PUSH_DELIM, 1)[0].strip() 701 targets.add(os.path.join(rel_out_dir, sanitized_target)) 702 return targets 703 704 705def _specified_bitness(xml_root): 706 """Check if the xml file contains the option append-bitness. 707 708 Args: 709 xml_root: abs path to xml file. 710 711 Returns: 712 True if xml specifies to append-bitness, False otherwise. 713 """ 714 option_tags = xml_root.findall('.//option') 715 for tag in option_tags: 716 value = tag.attrib[_XML_VALUE].strip() 717 name = tag.attrib[_XML_NAME].strip() 718 if name == _VTS_BITNESS and value == _VTS_BITNESS_TRUE: 719 return True 720 return False 721 722 723def _get_vts_binary_src_target(value, rel_out_dir): 724 """Parse out the vts10 binary src target. 725 726 The value can be in the following pattern: 727 - {_32bit,_64bit,_IPC32_32bit}::DATA/target (DATA/target) 728 - DATA/target->/data/target (DATA/target) 729 - out/host/linx-x86/bin/VtsSecuritySelinuxPolicyHostTest (the string as 730 is) 731 732 Args: 733 value: String of the XML option value to parse. 734 rel_out_dir: String path of out dir to prepend to target when required. 735 736 Returns: 737 String of the target to build. 738 """ 739 # We'll assume right off the bat we can use the value as is and modify it if 740 # necessary, e.g. out/host/linux-x86/bin... 741 target = value 742 # _32bit::DATA/target 743 match = _VTS_BINARY_SRC_DELIM_RE.match(value) 744 if match: 745 target = os.path.join(rel_out_dir, match.group('target')) 746 # DATA/target->/data/target 747 elif _XML_PUSH_DELIM in value: 748 target = value.split(_XML_PUSH_DELIM, 1)[0].strip() 749 target = os.path.join(rel_out_dir, target) 750 return target 751 752 753def get_plans_from_vts_xml(xml_file): 754 """Get configs which are included by xml_file. 755 756 We're looking for option(include) to get all dependency plan configs. 757 758 Args: 759 xml_file: Absolute path to xml file. 760 761 Returns: 762 A set of plan config paths which are depended by xml_file. 763 """ 764 if not os.path.exists(xml_file): 765 raise atest_error.XmlNotExistError( 766 '%s: The xml file doesnot exist' % xml_file 767 ) 768 plans = set() 769 xml_root = ET.parse(xml_file).getroot() 770 plans.add(xml_file) 771 option_tags = xml_root.findall('.//include') 772 if not option_tags: 773 return plans 774 # Currently, all vts10 xmls live in the same dir : 775 # https://android.googlesource.com/platform/test/vts/+/master/tools/vts-tradefed/res/config/ 776 # If the vts10 plans start using folders to organize the plans, the logic here 777 # should be changed. 778 xml_dir = os.path.dirname(xml_file) 779 for tag in option_tags: 780 name = tag.attrib[_XML_NAME].strip() 781 plans |= get_plans_from_vts_xml(os.path.join(xml_dir, name + '.xml')) 782 return plans 783 784 785def get_targets_from_vts_xml(xml_file, rel_out_dir, module_info): 786 """Parse a vts10 xml for test dependencies we need to build. 787 788 We have a separate vts10 parsing function because we make a big assumption 789 on the targets (the way they're formatted and what they represent) and we 790 also create these build targets in a very special manner as well. 791 The 6 options we're looking for are: 792 - binary-test-source 793 - push-group 794 - push 795 - test-module-name 796 - test-file-name 797 - apk 798 799 Args: 800 module_info: ModuleInfo class used to verify targets are valid modules. 801 rel_out_dir: Abs path to the out dir to help create vts10 build targets. 802 xml_file: abs path to xml file. 803 804 Returns: 805 A set of build targets based on the signals found in the xml file. 806 """ 807 xml_root = ET.parse(xml_file).getroot() 808 targets = set() 809 option_tags = xml_root.findall('.//option') 810 for tag in option_tags: 811 value = tag.attrib[_XML_VALUE].strip() 812 name = tag.attrib[_XML_NAME].strip() 813 if name in [_VTS_TEST_MODULE, _VTS_MODULE]: 814 if module_info.is_module(value): 815 targets.add(value) 816 else: 817 logging.debug( 818 'vts10 test module (%s) not present in module info, skipping build', 819 value, 820 ) 821 elif name == _VTS_BINARY_SRC: 822 targets.add(_get_vts_binary_src_target(value, rel_out_dir)) 823 elif name == _VTS_PUSH_GROUP: 824 # Look up the push file and parse out build artifacts (as well as 825 # other push group files to parse). 826 targets |= _get_vts_push_group_targets(value, rel_out_dir) 827 elif name == _VTS_PUSH: 828 # Parse out the build artifact directly. 829 push_target = value.split(_XML_PUSH_DELIM, 1)[0].strip() 830 # If the config specified append-bitness, append the bits suffixes 831 # to the target. 832 if _specified_bitness(xml_root): 833 targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_32)) 834 targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_64)) 835 else: 836 targets.add(os.path.join(rel_out_dir, push_target)) 837 elif name == _VTS_TEST_FILE: 838 # The _VTS_TEST_FILE values can be set in 2 possible ways: 839 # 1. test_file.apk 840 # 2. DATA/app/test_file/test_file.apk 841 # We'll assume that test_file.apk (#1) is in an expected path (but 842 # that is not true, see b/76158619) and create the full path for it 843 # and then append the _VTS_TEST_FILE value to targets to build. 844 target = os.path.join(rel_out_dir, value) 845 # If value is just an APK, specify the path that we expect it to be in 846 # e.g. 847 # out/host/linux-x86/vts10/android-vts10/testcases/DATA/app/test_file/test_file.apk 848 head, _ = os.path.split(value) 849 if not head: 850 target = os.path.join( 851 rel_out_dir, _VTS_OUT_DATA_APP_PATH, _get_apk_target(value), value 852 ) 853 targets.add(target) 854 elif name == _VTS_APK: 855 targets.add(os.path.join(rel_out_dir, value)) 856 logging.debug('Targets found in config file: %s', targets) 857 return targets 858 859 860def get_dir_path_and_filename(path): 861 """Return tuple of dir and file name from given path. 862 863 Args: 864 path: String of path to break up. 865 866 Returns: 867 Tuple of (dir, file) paths. 868 """ 869 if os.path.isfile(path): 870 dir_path, file_path = os.path.split(path) 871 else: 872 dir_path, file_path = path, None 873 return dir_path, file_path 874 875 876def search_integration_dirs(name, int_dirs): 877 """Search integration dirs for name and return full path. 878 879 Args: 880 name: A string of plan name needed to be found. 881 int_dirs: A list of path needed to be searched. 882 883 Returns: 884 A list of the test path. 885 Ask user to select if multiple tests are found. 886 None if no matched test found. 887 """ 888 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 889 test_files = [] 890 for integration_dir in int_dirs: 891 abs_path = os.path.join(root_dir, integration_dir) 892 test_paths = run_find_cmd(TestReferenceType.INTEGRATION, abs_path, name) 893 if test_paths: 894 test_files.extend(test_paths) 895 return extract_selected_tests(test_files) 896 897 898def get_int_dir_from_path(path, int_dirs): 899 """Search integration dirs for the given path and return path of dir. 900 901 Args: 902 path: A string of path needed to be found. 903 int_dirs: A list of path needed to be searched. 904 905 Returns: 906 A string of the test dir. None if no matched path found. 907 """ 908 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 909 if not os.path.exists(path): 910 return None 911 dir_path, file_name = get_dir_path_and_filename(path) 912 int_dir = None 913 for possible_dir in int_dirs: 914 abs_int_dir = os.path.join(root_dir, possible_dir) 915 if is_equal_or_sub_dir(dir_path, abs_int_dir): 916 int_dir = abs_int_dir 917 break 918 if not file_name: 919 logging.debug( 920 'Found dir (%s) matching input (%s).' 921 ' Referencing an entire Integration/Suite dir' 922 ' is not supported. If you are trying to reference' 923 ' a test by its path, please input the path to' 924 ' the integration/suite config file itself.', 925 int_dir, 926 path, 927 ) 928 return None 929 return int_dir 930 931 932def get_install_locations(installed_paths): 933 """Get install locations from installed paths. 934 935 Args: 936 installed_paths: List of installed_paths from module_info. 937 938 Returns: 939 Set of install locations from module_info installed_paths. e.g. 940 set(['host', 'device']) 941 """ 942 install_locations = set() 943 for path in installed_paths: 944 if _HOST_PATH_RE.match(path): 945 install_locations.add(constants.DEVICELESS_TEST) 946 elif _DEVICE_PATH_RE.match(path): 947 install_locations.add(constants.DEVICE_TEST) 948 return install_locations 949 950 951def get_levenshtein_distance( 952 test_name, module_name, dir_costs=constants.COST_TYPO 953): 954 """Return an edit distance between test_name and module_name. 955 956 Levenshtein Distance has 3 actions: delete, insert and replace. 957 dis_costs makes each action weigh differently. 958 959 Args: 960 test_name: A keyword from the users. 961 module_name: A testable module name. 962 dir_costs: A tuple which contains 3 integer, where dir represents 963 Deletion, Insertion and Replacement respectively. For guessing typos: 964 (1, 1, 1) gives the best result. For searching keywords, (8, 1, 5) gives 965 the best result. 966 967 Returns: 968 An edit distance integer between test_name and module_name. 969 """ 970 rows = len(test_name) + 1 971 cols = len(module_name) + 1 972 deletion, insertion, replacement = dir_costs 973 974 # Creating a Dynamic Programming Matrix and weighting accordingly. 975 dp_matrix = [[0 for _ in range(cols)] for _ in range(rows)] 976 # Weigh rows/deletion 977 for row in range(1, rows): 978 dp_matrix[row][0] = row * deletion 979 # Weigh cols/insertion 980 for col in range(1, cols): 981 dp_matrix[0][col] = col * insertion 982 # The core logic of LD 983 for col in range(1, cols): 984 for row in range(1, rows): 985 if test_name[row - 1] == module_name[col - 1]: 986 cost = 0 987 else: 988 cost = replacement 989 dp_matrix[row][col] = min( 990 dp_matrix[row - 1][col] + deletion, 991 dp_matrix[row][col - 1] + insertion, 992 dp_matrix[row - 1][col - 1] + cost, 993 ) 994 995 return dp_matrix[row][col] 996 997 998def is_test_from_kernel_xml(xml_file, test_name): 999 """Check if test defined in xml_file. 1000 1001 A kernel test can be defined like: 1002 <option name="test-command-line" key="test_class_1" value="command 1" /> 1003 where key is the name of test class and method of the runner. This method 1004 returns True if the test_name was defined in the given xml_file. 1005 1006 Args: 1007 xml_file: Absolute path to xml file. 1008 test_name: test_name want to find. 1009 1010 Returns: 1011 True if test_name in xml_file, False otherwise. 1012 """ 1013 if not os.path.exists(xml_file): 1014 return False 1015 xml_root = ET.parse(xml_file).getroot() 1016 option_tags = xml_root.findall('.//option') 1017 for option_tag in option_tags: 1018 if option_tag.attrib['name'] == 'test-command-line': 1019 if option_tag.attrib['key'] == test_name: 1020 return True 1021 return False 1022 1023 1024def get_java_methods(test_path): 1025 """Find out the java test class of input test_path. 1026 1027 Args: 1028 test_path: A string of absolute path to the java file. 1029 1030 Returns: 1031 A set of methods. 1032 """ 1033 logging.debug('Probing %s:', test_path) 1034 with open(test_path) as class_file: 1035 content = class_file.read() 1036 matches = re.findall(_JAVA_METHODS_RE, content) 1037 if matches: 1038 methods = {match[1] for match in matches} 1039 logging.debug('Available methods: %s\n', methods) 1040 return methods 1041 return set() 1042 1043 1044@contextmanager 1045def open_cc(filename: str): 1046 """Open a cc/cpp file with comments trimmed.""" 1047 target_cc = filename 1048 if shutil.which('gcc'): 1049 tmp = tempfile.NamedTemporaryFile() 1050 cmd = f'gcc -fpreprocessed -dD -E {filename} > {tmp.name}' 1051 strip_proc = subprocess.run(cmd, shell=True, check=False) 1052 if strip_proc.returncode == ExitCode.SUCCESS: 1053 target_cc = tmp.name 1054 else: 1055 logging.debug( 1056 'Failed to strip comments in %s. Parsing ' 1057 'class/method name may not be accurate.', 1058 target_cc, 1059 ) 1060 else: 1061 logging.debug('Cannot find "gcc" and unable to trim comments.') 1062 try: 1063 cc_obj = open(target_cc, 'r') 1064 yield cc_obj 1065 finally: 1066 cc_obj.close() 1067 1068 1069# pylint: disable=too-many-branches 1070def get_cc_class_info(test_path): 1071 """Get the class info of the given cc input test_path. 1072 1073 The class info dict will be like: 1074 {'classA': { 1075 'methods': {'m1', 'm2'}, 'prefixes': {'pfx1'}, 'typed': True}, 1076 'classB': { 1077 'methods': {'m3', 'm4'}, 'prefixes': set(), 'typed': False}, 1078 'classC': { 1079 'methods': {'m5', 'm6'}, 'prefixes': set(), 'typed': True}, 1080 'classD': { 1081 'methods': {'m7', 'm8'}, 'prefixes': {'pfx3'}, 'typed': False}} 1082 According to the class info, we can tell that: 1083 classA is a typed-parameterized test. (TYPED_TEST_SUITE_P) 1084 classB is a regular gtest. (TEST_F|TEST) 1085 classC is a typed test. (TYPED_TEST_SUITE) 1086 classD is a value-parameterized test. (TEST_P) 1087 1088 Args: 1089 test_path: A string of absolute path to the cc file. 1090 1091 Returns: 1092 A dict of class info. 1093 """ 1094 with open_cc(test_path) as class_file: 1095 content = class_file.read() 1096 logging.debug('Parsing: %s', test_path) 1097 class_info, no_test_classes = test_filter_utils.get_cc_class_info(content) 1098 1099 if no_test_classes: 1100 metrics.LocalDetectEvent( 1101 detect_type=DetectType.NATIVE_TEST_NOT_FOUND, 1102 result=DetectType.NATIVE_TEST_NOT_FOUND, 1103 ) 1104 1105 return class_info 1106 1107 1108def find_host_unit_tests(module_info, path): 1109 """Find host unit tests for the input path. 1110 1111 Args: 1112 module_info: ModuleInfo obj. 1113 path: A string of the relative path from $ANDROID_BUILD_TOP that we want 1114 to search. 1115 1116 Returns: 1117 A list that includes the module name of host unit tests, otherwise an 1118 empty 1119 list. 1120 """ 1121 logging.debug('finding host unit tests under %s', path) 1122 host_unit_test_names = module_info.get_all_host_unit_tests() 1123 logging.debug('All the host unit tests: %s', host_unit_test_names) 1124 1125 # Return all tests if the path relative to ${ANDROID_BUILD_TOP} is '.'. 1126 if path == '.': 1127 return host_unit_test_names 1128 1129 tests = [] 1130 for name in host_unit_test_names: 1131 for test_path in module_info.get_paths(name): 1132 if test_path.find(path) == 0: 1133 tests.append(name) 1134 return tests 1135 1136 1137def get_annotated_methods(annotation, file_path): 1138 """Find all the methods annotated by the input annotation in the file_path. 1139 1140 Args: 1141 annotation: A string of the annotation class. 1142 file_path: A string of the file path. 1143 1144 Returns: 1145 A set of all the methods annotated. 1146 """ 1147 methods = set() 1148 annotation_name = '@' + str(annotation).split('.')[-1] 1149 with open(file_path) as class_file: 1150 enter_annotation_block = False 1151 for line in class_file: 1152 if str(line).strip().startswith(annotation_name): 1153 enter_annotation_block = True 1154 continue 1155 if enter_annotation_block: 1156 matches = re.findall(_JAVA_METHODS_RE, line) 1157 if matches: 1158 methods.update({match[1] for match in matches}) 1159 enter_annotation_block = False 1160 continue 1161 return methods 1162 1163 1164def get_test_config_and_srcs(test_info, module_info): 1165 """Get the test config path for the input test_info. 1166 1167 The search rule will be: 1168 Check if test name in test_info could be found in module_info 1169 1. AndroidTest.xml under module path if no test config be set. 1170 2. The first test config defined in Android.bp if test config be set. 1171 If test name could not found matched module in module_info, search all the 1172 test config name if match. 1173 1174 Args: 1175 test_info: TestInfo obj. 1176 module_info: ModuleInfo obj. 1177 1178 Returns: 1179 A string of the config path and list of srcs, None if test config not 1180 exist. 1181 """ 1182 test_name = test_info.test_name 1183 mod_info = module_info.get_module_info(test_name) 1184 1185 if mod_info: 1186 get_config_srcs_tuple = _get_config_srcs_tuple_from_module_info 1187 ref_obj = mod_info 1188 else: 1189 # For tests that the configs were generated by soong and the test_name 1190 # cannot be found in module_info. 1191 get_config_srcs_tuple = _get_config_srcs_tuple_when_no_module_info 1192 ref_obj = module_info 1193 1194 config_src_tuple = get_config_srcs_tuple(ref_obj, test_name) 1195 return config_src_tuple if config_src_tuple else (None, None) 1196 1197 1198def _get_config_srcs_tuple_from_module_info( 1199 mod_info: Dict[str, Any], _=None 1200) -> Tuple[str, List[str]]: 1201 """Get test config and srcs from the given info of the module.""" 1202 android_root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 1203 test_configs = mod_info.get(constants.MODULE_TEST_CONFIG, []) 1204 if len(test_configs) == 0: 1205 # Check for AndroidTest.xml at the module path. 1206 for path in mod_info.get(constants.MODULE_PATH, []): 1207 config_path = os.path.join( 1208 android_root_dir, path, constants.MODULE_CONFIG 1209 ) 1210 if os.path.isfile(config_path): 1211 return config_path, mod_info.get(constants.MODULE_SRCS, []) 1212 if len(test_configs) >= 1: 1213 test_config = test_configs[0] 1214 config_path = os.path.join(android_root_dir, test_config) 1215 if os.path.isfile(config_path): 1216 return config_path, mod_info.get(constants.MODULE_SRCS, []) 1217 return None, None 1218 1219 1220def _get_config_srcs_tuple_when_no_module_info( 1221 module_info_obj: module_info.ModuleInfo, test_name: str 1222) -> Tuple[Path, List[str]]: 1223 """Get test config and srcs by iterating the whole module_info.""" 1224 1225 def get_config_srcs(info: Dict[str, Any], test_name: str): 1226 test_configs = info.get(constants.MODULE_TEST_CONFIG, []) 1227 for test_config in test_configs: 1228 config_path = atest_utils.get_build_top(test_config) 1229 config_name = config_path.stem 1230 if config_name == test_name and os.path.isfile(config_path): 1231 return config_path, info.get(constants.MODULE_SRCS, []) 1232 return None, None 1233 1234 infos = ( 1235 module_info_obj.get_module_info(mod) 1236 for mod in module_info_obj.get_testable_modules() 1237 ) 1238 1239 for info in infos: 1240 results = get_config_srcs(info, test_name) 1241 if any(results): 1242 return results 1243 return None, None 1244 1245 1246def need_aggregate_metrics_result(test_xml: str) -> bool: 1247 """Check if input test config need aggregate metrics. 1248 1249 If the input test define metrics_collector, which means there's a need for 1250 atest to have the aggregate metrics result. 1251 1252 Args: 1253 test_xml: A string of the path for the test xml. 1254 1255 Returns: 1256 True if input test need to enable aggregate metrics result. 1257 """ 1258 # Due to (b/211640060) it may replace .xml with .config in the xml as 1259 # workaround. 1260 if not Path(test_xml).is_file(): 1261 if Path(test_xml).suffix == '.config': 1262 test_xml = test_xml.rsplit('.', 1)[0] + '.xml' 1263 1264 if Path(test_xml).is_file(): 1265 xml_root = ET.parse(test_xml).getroot() 1266 if xml_root.findall('.//metrics_collector'): 1267 return True 1268 # Recursively check included configs in the same git repository. 1269 git_dir = get_git_path(test_xml) 1270 include_configs = xml_root.findall('.//include') 1271 for include_config in include_configs: 1272 name = include_config.attrib[_XML_NAME].strip() 1273 # Get the absolute path for the included configs. 1274 include_paths = search_integration_dirs( 1275 os.path.splitext(name)[0], [git_dir] 1276 ) 1277 for include_path in include_paths: 1278 if need_aggregate_metrics_result(include_path): 1279 return True 1280 return False 1281 1282 1283def get_git_path(file_path: str) -> str: 1284 """Get the path of the git repository for the input file. 1285 1286 Args: 1287 file_path: A string of the path to find the git path it belongs. 1288 1289 Returns: 1290 The path of the git repository for the input file, return the path of 1291 $ANDROID_BUILD_TOP if nothing find. 1292 """ 1293 build_top = os.environ.get(constants.ANDROID_BUILD_TOP) 1294 parent = Path(file_path).absolute().parent 1295 while not parent.samefile('/') and not parent.samefile(build_top): 1296 if parent.joinpath('.git').is_dir(): 1297 return parent.absolute() 1298 parent = parent.parent 1299 return build_top 1300 1301 1302def parse_test_reference(test_ref: str) -> Dict[str, str]: 1303 """Parse module, class/pkg, and method name from the given test reference. 1304 1305 The result will be a none empty dictionary only if input test reference 1306 match $module:$pkg_class or $module:$pkg_class:$method. 1307 1308 Args: 1309 test_ref: A string of the input test reference from command line. 1310 1311 Returns: 1312 Dict includes module_name, pkg_class_name and method_name. 1313 """ 1314 ref_match = re.match( 1315 r'^(?P<module_name>[^:#]+):(?P<pkg_class_name>[^#]+)' 1316 r'#?(?P<method_name>.*)$', 1317 test_ref, 1318 ) 1319 1320 return ref_match.groupdict(default=dict()) if ref_match else dict() 1321