1# Copyright 2014 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5 6 7import html.parser 8import json 9import logging 10import os 11import re 12import tempfile 13import threading 14import xml.etree.ElementTree 15 16from devil.android import apk_helper 17from pylib import constants 18from pylib.constants import host_paths 19from pylib.base import base_test_result 20from pylib.base import test_instance 21from pylib.symbols import stack_symbolizer 22from pylib.utils import test_filter 23 24with host_paths.SysPath(host_paths.BUILD_UTIL_PATH): 25 from lib.common import unittest_util 26 27BROWSER_TEST_SUITES = [ 28 'android_browsertests', 29 'android_sync_integration_tests', 30 'components_browsertests', 31 'content_browsertests', 32 'weblayer_browsertests', 33] 34 35# The max number of tests to run on a shard during the test run. 36MAX_SHARDS = 256 37 38RUN_IN_SUB_THREAD_TEST_SUITES = [ 39 # Multiprocess tests should be run outside of the main thread. 40 'base_unittests', # file_locking_unittest.cc uses a child process. 41 'gwp_asan_unittests', 42 'ipc_perftests', 43 'ipc_tests', 44 'mojo_perftests', 45 'mojo_unittests', 46 'net_unittests' 47] 48 49 50# Used for filtering large data deps at a finer grain than what's allowed in 51# isolate files since pushing deps to devices is expensive. 52# Wildcards are allowed. 53_DEPS_EXCLUSION_LIST = [ 54 'chrome/test/data/extensions/api_test', 55 'chrome/test/data/extensions/secure_shell', 56 'chrome/test/data/firefox*', 57 'chrome/test/data/gpu', 58 'chrome/test/data/image_decoding', 59 'chrome/test/data/import', 60 'chrome/test/data/page_cycler', 61 'chrome/test/data/perf', 62 'chrome/test/data/pyauto_private', 63 'chrome/test/data/safari_import', 64 'chrome/test/data/scroll', 65 'chrome/test/data/third_party', 66 'third_party/hunspell_dictionaries/*.dic', 67 # crbug.com/258690 68 'webkit/data/bmp_decoder', 69 'webkit/data/ico_decoder', 70] 71 72 73_EXTRA_NATIVE_TEST_ACTIVITY = ( 74 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' 75 'NativeTestActivity') 76_EXTRA_RUN_IN_SUB_THREAD = ( 77 'org.chromium.native_test.NativeTest.RunInSubThread') 78EXTRA_SHARD_NANO_TIMEOUT = ( 79 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' 80 'ShardNanoTimeout') 81_EXTRA_SHARD_SIZE_LIMIT = ( 82 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' 83 'ShardSizeLimit') 84 85# TODO(jbudorick): Remove these once we're no longer parsing stdout to generate 86# results. 87_RE_TEST_STATUS = re.compile( 88 # Test state. 89 r'\[ +((?:RUN)|(?:FAILED)|(?:OK)|(?:CRASHED)|(?:SKIPPED)) +\] ?' 90 # Test name. 91 r'([^ ]+)?' 92 # Optional parameters. 93 r'(?:, where' 94 # Type parameter 95 r'(?: TypeParam = [^()]*(?: and)?)?' 96 # Value parameter 97 r'(?: GetParam\(\) = [^()]*)?' 98 # End of optional parameters. 99 ')?' 100 # Optional test execution time. 101 r'(?: \((\d+) ms\))?$') 102# Crash detection constants. 103_RE_TEST_ERROR = re.compile(r'FAILURES!!! Tests run: \d+,' 104 r' Failures: \d+, Errors: 1') 105_RE_TEST_CURRENTLY_RUNNING = re.compile( 106 r'\[.*ERROR:.*?\] Currently running: (.*)') 107_RE_TEST_DCHECK_FATAL = re.compile(r'\[.*:FATAL:.*\] (.*)') 108_RE_DISABLED = re.compile(r'DISABLED_') 109_RE_FLAKY = re.compile(r'FLAKY_') 110 111# Detect a new launcher invocation. When encountered, the output parser will 112# stop recording logs for a suddenly crashed test (if one was running) in the 113# previous invocation. 114_RE_LAUNCHER_MAIN_START = re.compile(r'>>ScopedMainEntryLogger') 115 116# Regex that matches the printout when there are test failures. 117# matches "[ FAILED ] 1 test, listed below:" 118_RE_ANY_TESTS_FAILED = re.compile(r'\[ +FAILED +\].*listed below') 119 120# Detect stack line in stdout. 121_STACK_LINE_RE = re.compile(r'\s*#\d+') 122 123def ParseGTestListTests(raw_list): 124 """Parses a raw test list as provided by --gtest_list_tests. 125 126 Args: 127 raw_list: The raw test listing with the following format: 128 129 IPCChannelTest. 130 SendMessageInChannelConnected 131 IPCSyncChannelTest. 132 Simple 133 DISABLED_SendWithTimeoutMixedOKAndTimeout 134 135 Returns: 136 A list of all tests. For the above raw listing: 137 138 [IPCChannelTest.SendMessageInChannelConnected, IPCSyncChannelTest.Simple, 139 IPCSyncChannelTest.DISABLED_SendWithTimeoutMixedOKAndTimeout] 140 """ 141 ret = [] 142 current = '' 143 for test in raw_list: 144 if not test: 145 continue 146 if not test.startswith(' '): 147 test_case = test.split()[0] 148 if test_case.endswith('.'): 149 current = test_case 150 else: 151 test = test.strip() 152 if test and not 'YOU HAVE' in test: 153 test_name = test.split()[0] 154 ret += [current + test_name] 155 return ret 156 157 158def ParseGTestOutput(output, symbolizer, device_abi): 159 """Parses raw gtest output and returns a list of results. 160 161 Args: 162 output: A list of output lines. 163 symbolizer: The symbolizer used to symbolize stack. 164 device_abi: Device abi that is needed for symbolization. 165 Returns: 166 A list of base_test_result.BaseTestResults. 167 """ 168 duration = 0 169 fallback_result_type = None 170 log = [] 171 stack = [] 172 result_type = None 173 results = [] 174 test_name = None 175 176 def symbolize_stack_and_merge_with_log(): 177 log_string = '\n'.join(log or []) 178 if not stack: 179 stack_string = '' 180 else: 181 stack_string = '\n'.join( 182 symbolizer.ExtractAndResolveNativeStackTraces( 183 stack, device_abi)) 184 return '%s\n%s' % (log_string, stack_string) 185 186 def handle_possibly_unknown_test(): 187 if test_name is not None: 188 results.append( 189 base_test_result.BaseTestResult( 190 TestNameWithoutDisabledPrefix(test_name), 191 # If we get here, that means we started a test, but it did not 192 # produce a definitive test status output, so assume it crashed. 193 # crbug/1191716 194 fallback_result_type or base_test_result.ResultType.CRASH, 195 duration, 196 log=symbolize_stack_and_merge_with_log())) 197 198 for l in output: 199 matcher = _RE_TEST_STATUS.match(l) 200 launcher_main_start_match = _RE_LAUNCHER_MAIN_START.match(l) 201 if matcher: 202 if matcher.group(1) == 'RUN': 203 handle_possibly_unknown_test() 204 duration = 0 205 fallback_result_type = None 206 log = [] 207 stack = [] 208 result_type = None 209 elif matcher.group(1) == 'OK': 210 result_type = base_test_result.ResultType.PASS 211 elif matcher.group(1) == 'SKIPPED': 212 result_type = base_test_result.ResultType.SKIP 213 elif matcher.group(1) == 'FAILED': 214 result_type = base_test_result.ResultType.FAIL 215 elif matcher.group(1) == 'CRASHED': 216 fallback_result_type = base_test_result.ResultType.CRASH 217 # Be aware that test name and status might not appear on same line. 218 test_name = matcher.group(2) if matcher.group(2) else test_name 219 duration = int(matcher.group(3)) if matcher.group(3) else 0 220 221 else: 222 # Can possibly add more matchers, such as different results from DCHECK. 223 currently_running_matcher = _RE_TEST_CURRENTLY_RUNNING.match(l) 224 dcheck_matcher = _RE_TEST_DCHECK_FATAL.match(l) 225 226 if currently_running_matcher: 227 test_name = currently_running_matcher.group(1) 228 result_type = base_test_result.ResultType.CRASH 229 duration = None # Don't know. Not using 0 as this is unknown vs 0. 230 elif dcheck_matcher or launcher_main_start_match: 231 result_type = base_test_result.ResultType.CRASH 232 duration = None # Don't know. Not using 0 as this is unknown vs 0. 233 234 if not launcher_main_start_match: 235 if not matcher and _STACK_LINE_RE.match(l): 236 stack.append(l) 237 else: 238 log.append(l) 239 240 if _RE_ANY_TESTS_FAILED.match(l): 241 break 242 243 if result_type and test_name: 244 # Don't bother symbolizing output if the test passed. 245 if result_type == base_test_result.ResultType.PASS: 246 stack = [] 247 results.append(base_test_result.BaseTestResult( 248 TestNameWithoutDisabledPrefix(test_name), result_type, duration, 249 log=symbolize_stack_and_merge_with_log())) 250 test_name = None 251 252 else: 253 # Executing this after tests have finished with a failure causes a 254 # duplicate test entry to be added to results. crbug/1380825 255 handle_possibly_unknown_test() 256 257 return results 258 259 260def ParseGTestXML(xml_content): 261 """Parse gtest XML result.""" 262 results = [] 263 if not xml_content: 264 return results 265 266 html_parser = html.parser.HTMLParser() 267 268 testsuites = xml.etree.ElementTree.fromstring(xml_content) 269 for testsuite in testsuites: 270 suite_name = testsuite.attrib['name'] 271 for testcase in testsuite: 272 case_name = testcase.attrib['name'] 273 result_type = base_test_result.ResultType.PASS 274 log = [] 275 for failure in testcase: 276 result_type = base_test_result.ResultType.FAIL 277 log.append(html_parser.unescape(failure.attrib['message'])) 278 279 results.append(base_test_result.BaseTestResult( 280 '%s.%s' % (suite_name, TestNameWithoutDisabledPrefix(case_name)), 281 result_type, 282 int(float(testcase.attrib['time']) * 1000), 283 log=('\n'.join(log) if log else ''))) 284 285 return results 286 287 288def ParseGTestJSON(json_content): 289 """Parse results in the JSON Test Results format.""" 290 results = [] 291 if not json_content: 292 return results 293 294 json_data = json.loads(json_content) 295 296 openstack = list(json_data['tests'].items()) 297 298 while openstack: 299 name, value = openstack.pop() 300 301 if 'expected' in value and 'actual' in value: 302 if value['actual'] == 'PASS': 303 result_type = base_test_result.ResultType.PASS 304 elif value['actual'] == 'SKIP': 305 result_type = base_test_result.ResultType.SKIP 306 elif value['actual'] == 'CRASH': 307 result_type = base_test_result.ResultType.CRASH 308 elif value['actual'] == 'TIMEOUT': 309 result_type = base_test_result.ResultType.TIMEOUT 310 else: 311 result_type = base_test_result.ResultType.FAIL 312 results.append(base_test_result.BaseTestResult(name, result_type)) 313 else: 314 openstack += [("%s.%s" % (name, k), v) for k, v in value.items()] 315 316 return results 317 318 319def TestNameWithoutDisabledPrefix(test_name): 320 """Modify the test name without disabled prefix if prefix 'DISABLED_' or 321 'FLAKY_' presents. 322 323 Args: 324 test_name: The name of a test. 325 Returns: 326 A test name without prefix 'DISABLED_' or 'FLAKY_'. 327 """ 328 disabled_prefixes = [_RE_DISABLED, _RE_FLAKY] 329 for dp in disabled_prefixes: 330 test_name = dp.sub('', test_name) 331 return test_name 332 333class GtestTestInstance(test_instance.TestInstance): 334 335 def __init__(self, args, data_deps_delegate, error_func): 336 super().__init__() 337 # TODO(jbudorick): Support multiple test suites. 338 if len(args.suite_name) > 1: 339 raise ValueError('Platform mode currently supports only 1 gtest suite') 340 self._additional_apks = [] 341 self._coverage_dir = args.coverage_dir 342 self._exe_dist_dir = None 343 self._external_shard_index = args.test_launcher_shard_index 344 self._extract_test_list_from_filter = args.extract_test_list_from_filter 345 self._filter_tests_lock = threading.Lock() 346 self._gs_test_artifacts_bucket = args.gs_test_artifacts_bucket 347 self._isolated_script_test_output = args.isolated_script_test_output 348 self._isolated_script_test_perf_output = ( 349 args.isolated_script_test_perf_output) 350 self._render_test_output_dir = args.render_test_output_dir 351 self._shard_timeout = args.shard_timeout 352 self._store_tombstones = args.store_tombstones 353 self._suite = args.suite_name[0] 354 self._symbolizer = stack_symbolizer.Symbolizer(None) 355 self._total_external_shards = args.test_launcher_total_shards 356 self._wait_for_java_debugger = args.wait_for_java_debugger 357 self._use_existing_test_data = args.use_existing_test_data 358 359 # GYP: 360 if args.executable_dist_dir: 361 self._exe_dist_dir = os.path.abspath(args.executable_dist_dir) 362 else: 363 # TODO(agrieve): Remove auto-detection once recipes pass flag explicitly. 364 exe_dist_dir = os.path.join(constants.GetOutDirectory(), 365 '%s__dist' % self._suite) 366 367 if os.path.exists(exe_dist_dir): 368 self._exe_dist_dir = exe_dist_dir 369 370 incremental_part = '' 371 if args.test_apk_incremental_install_json: 372 incremental_part = '_incremental' 373 374 self._test_launcher_batch_limit = MAX_SHARDS 375 if (args.test_launcher_batch_limit 376 and 0 < args.test_launcher_batch_limit < MAX_SHARDS): 377 self._test_launcher_batch_limit = args.test_launcher_batch_limit 378 379 apk_path = os.path.join( 380 constants.GetOutDirectory(), '%s_apk' % self._suite, 381 '%s-debug%s.apk' % (self._suite, incremental_part)) 382 self._test_apk_incremental_install_json = ( 383 args.test_apk_incremental_install_json) 384 if not os.path.exists(apk_path): 385 self._apk_helper = None 386 else: 387 self._apk_helper = apk_helper.ApkHelper(apk_path) 388 self._extras = { 389 _EXTRA_NATIVE_TEST_ACTIVITY: self._apk_helper.GetActivityName(), 390 } 391 if args.timeout_scale and args.timeout_scale != 1: 392 self._extras[_EXTRA_RUN_IN_SUB_THREAD] = 1 393 394 if self._suite in RUN_IN_SUB_THREAD_TEST_SUITES: 395 self._extras[_EXTRA_RUN_IN_SUB_THREAD] = 1 396 if self._suite in BROWSER_TEST_SUITES: 397 self._extras[_EXTRA_SHARD_SIZE_LIMIT] = 1 398 self._extras[EXTRA_SHARD_NANO_TIMEOUT] = int(1e9 * self._shard_timeout) 399 self._shard_timeout = 10 * self._shard_timeout 400 if args.wait_for_java_debugger: 401 self._extras[EXTRA_SHARD_NANO_TIMEOUT] = int(1e15) # Forever 402 403 if not self._apk_helper and not self._exe_dist_dir: 404 error_func('Could not find apk or executable for %s' % self._suite) 405 406 for x in args.additional_apks: 407 if not os.path.exists(x): 408 error_func('Could not find additional APK: %s' % x) 409 410 apk = apk_helper.ToHelper(x) 411 self._additional_apks.append(apk) 412 413 self._data_deps = [] 414 self._gtest_filters = test_filter.InitializeFiltersFromArgs(args) 415 self._run_disabled = args.run_disabled 416 self._run_pre_tests = args.run_pre_tests 417 418 self._data_deps_delegate = data_deps_delegate 419 self._runtime_deps_path = args.runtime_deps_path 420 if not self._runtime_deps_path: 421 logging.warning('No data dependencies will be pushed.') 422 423 if args.app_data_files: 424 self._app_data_files = args.app_data_files 425 if args.app_data_file_dir: 426 self._app_data_file_dir = args.app_data_file_dir 427 else: 428 self._app_data_file_dir = tempfile.mkdtemp() 429 logging.critical('Saving app files to %s', self._app_data_file_dir) 430 else: 431 self._app_data_files = None 432 self._app_data_file_dir = None 433 434 self._flags = None 435 self._initializeCommandLineFlags(args) 436 437 # TODO(jbudorick): Remove this once it's deployed. 438 self._enable_xml_result_parsing = args.enable_xml_result_parsing 439 440 def _initializeCommandLineFlags(self, args): 441 self._flags = [] 442 if args.command_line_flags: 443 self._flags.extend(args.command_line_flags) 444 if args.device_flags_file: 445 with open(args.device_flags_file) as f: 446 stripped_lines = (l.strip() for l in f) 447 self._flags.extend(flag for flag in stripped_lines if flag) 448 if args.run_disabled: 449 self._flags.append('--gtest_also_run_disabled_tests') 450 451 @property 452 def activity(self): 453 return self._apk_helper and self._apk_helper.GetActivityName() 454 455 @property 456 def additional_apks(self): 457 return self._additional_apks 458 459 @property 460 def apk(self): 461 return self._apk_helper and self._apk_helper.path 462 463 @property 464 def apk_helper(self): 465 return self._apk_helper 466 467 @property 468 def app_file_dir(self): 469 return self._app_data_file_dir 470 471 @property 472 def app_files(self): 473 return self._app_data_files 474 475 @property 476 def coverage_dir(self): 477 return self._coverage_dir 478 479 @property 480 def enable_xml_result_parsing(self): 481 return self._enable_xml_result_parsing 482 483 @property 484 def exe_dist_dir(self): 485 return self._exe_dist_dir 486 487 @property 488 def external_shard_index(self): 489 return self._external_shard_index 490 491 @property 492 def extract_test_list_from_filter(self): 493 return self._extract_test_list_from_filter 494 495 @property 496 def extras(self): 497 return self._extras 498 499 @property 500 def flags(self): 501 return self._flags 502 503 @property 504 def gs_test_artifacts_bucket(self): 505 return self._gs_test_artifacts_bucket 506 507 @property 508 def gtest_filters(self): 509 return self._gtest_filters 510 511 @property 512 def isolated_script_test_output(self): 513 return self._isolated_script_test_output 514 515 @property 516 def isolated_script_test_perf_output(self): 517 return self._isolated_script_test_perf_output 518 519 @property 520 def render_test_output_dir(self): 521 return self._render_test_output_dir 522 523 @property 524 def package(self): 525 return self._apk_helper and self._apk_helper.GetPackageName() 526 527 @property 528 def permissions(self): 529 return self._apk_helper and self._apk_helper.GetPermissions() 530 531 @property 532 def runner(self): 533 return self._apk_helper and self._apk_helper.GetInstrumentationName() 534 535 @property 536 def shard_timeout(self): 537 return self._shard_timeout 538 539 @property 540 def store_tombstones(self): 541 return self._store_tombstones 542 543 @property 544 def suite(self): 545 return self._suite 546 547 @property 548 def symbolizer(self): 549 return self._symbolizer 550 551 @property 552 def test_apk_incremental_install_json(self): 553 return self._test_apk_incremental_install_json 554 555 @property 556 def test_launcher_batch_limit(self): 557 return self._test_launcher_batch_limit 558 559 @property 560 def total_external_shards(self): 561 return self._total_external_shards 562 563 @property 564 def wait_for_java_debugger(self): 565 return self._wait_for_java_debugger 566 567 @property 568 def use_existing_test_data(self): 569 return self._use_existing_test_data 570 571 @property 572 def run_pre_tests(self): 573 return self._run_pre_tests 574 575 #override 576 def TestType(self): 577 return 'gtest' 578 579 #override 580 def GetPreferredAbis(self): 581 if not self._apk_helper: 582 return None 583 return self._apk_helper.GetAbis() 584 585 #override 586 def SetUp(self): 587 """Map data dependencies via isolate.""" 588 self._data_deps.extend( 589 self._data_deps_delegate(self._runtime_deps_path)) 590 591 def GetDataDependencies(self): 592 """Returns the test suite's data dependencies. 593 594 Returns: 595 A list of (host_path, device_path) tuples to push. If device_path is 596 None, the client is responsible for determining where to push the file. 597 """ 598 return self._data_deps 599 600 def FilterTests(self, test_list, disabled_prefixes=None): 601 """Filters |test_list| based on prefixes and, if present, a filter string. 602 603 Args: 604 test_list: The list of tests to filter. 605 disabled_prefixes: A list of test prefixes to filter. Defaults to 606 DISABLED_, FLAKY_, FAILS_, PRE_, and MANUAL_ 607 Returns: 608 A filtered list of tests to run. 609 """ 610 gtest_filter_strings = [ 611 self._GenerateDisabledFilterString(disabled_prefixes)] 612 if self._gtest_filters: 613 gtest_filter_strings.extend(self._gtest_filters) 614 615 filtered_test_list = test_list 616 # This lock is required because on older versions of Python 617 # |unittest_util.FilterTestNames| use of |fnmatch| is not threadsafe. 618 with self._filter_tests_lock: 619 for gtest_filter_string in gtest_filter_strings: 620 logging.debug('Filtering tests using: %s', gtest_filter_string) 621 filtered_test_list = unittest_util.FilterTestNames( 622 filtered_test_list, gtest_filter_string) 623 624 if self._run_disabled and self._gtest_filters: 625 out_filtered_test_list = list(set(test_list)-set(filtered_test_list)) 626 for test in out_filtered_test_list: 627 test_name_no_disabled = TestNameWithoutDisabledPrefix(test) 628 if test_name_no_disabled == test: 629 continue 630 if all( 631 unittest_util.FilterTestNames([test_name_no_disabled], 632 gtest_filter) 633 for gtest_filter in self._gtest_filters): 634 filtered_test_list.append(test) 635 return filtered_test_list 636 637 def _GenerateDisabledFilterString(self, disabled_prefixes): 638 disabled_filter_items = [] 639 640 if disabled_prefixes is None: 641 disabled_prefixes = ['FAILS_'] 642 if '--run-manual' not in self._flags: 643 disabled_prefixes += ['MANUAL_'] 644 if not self._run_disabled: 645 disabled_prefixes += ['DISABLED_', 'FLAKY_'] 646 if not self._run_pre_tests: 647 disabled_prefixes += ['PRE_'] 648 649 disabled_filter_items += ['%s*' % dp for dp in disabled_prefixes] 650 disabled_filter_items += ['*.%s*' % dp for dp in disabled_prefixes] 651 652 disabled_tests_file_path = os.path.join( 653 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'pylib', 'gtest', 654 'filter', '%s_disabled' % self._suite) 655 if disabled_tests_file_path and os.path.exists(disabled_tests_file_path): 656 with open(disabled_tests_file_path) as disabled_tests_file: 657 disabled_filter_items += [ 658 '%s' % l for l in (line.strip() for line in disabled_tests_file) 659 if l and not l.startswith('#')] 660 661 return '*-%s' % ':'.join(disabled_filter_items) 662 663 #override 664 def TearDown(self): 665 """Do nothing.""" 666