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