xref: /aosp_15_r20/external/cronet/build/android/pylib/gtest/gtest_test_instance.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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