xref: /aosp_15_r20/external/angle/build/android/pylib/gtest/gtest_test_instance.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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