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