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