1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import copy
7import logging
8import os
9import pickle
10import re
11
12import six
13from devil.android import apk_helper
14from pylib import constants
15from pylib.base import base_test_result
16from pylib.base import test_exception
17from pylib.base import test_instance
18from pylib.constants import host_paths
19from pylib.instrumentation import test_result
20from pylib.instrumentation import instrumentation_parser
21from pylib.symbols import deobfuscator
22from pylib.symbols import stack_symbolizer
23from pylib.utils import dexdump
24from pylib.utils import gold_utils
25from pylib.utils import instrumentation_tracing
26from pylib.utils import proguard
27from pylib.utils import shared_preference_utils
28from pylib.utils import test_filter
29
30
31with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
32  import unittest_util # pylint: disable=import-error
33
34# Ref: http://developer.android.com/reference/android/app/Activity.html
35_ACTIVITY_RESULT_CANCELED = 0
36_ACTIVITY_RESULT_OK = -1
37
38_COMMAND_LINE_PARAMETER = 'cmdlinearg-parameter'
39_DEFAULT_ANNOTATIONS = [
40    'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest', 'IntegrationTest']
41_EXCLUDE_UNLESS_REQUESTED_ANNOTATIONS = [
42    'DisabledTest', 'FlakyTest', 'Manual']
43_VALID_ANNOTATIONS = set(_DEFAULT_ANNOTATIONS +
44                         _EXCLUDE_UNLESS_REQUESTED_ANNOTATIONS)
45
46_TEST_LIST_JUNIT4_RUNNERS = [
47    'org.chromium.base.test.BaseChromiumAndroidJUnitRunner']
48
49_SKIP_PARAMETERIZATION = 'SkipCommandLineParameterization'
50_PARAMETERIZED_COMMAND_LINE_FLAGS = 'ParameterizedCommandLineFlags'
51_PARAMETERIZED_COMMAND_LINE_FLAGS_SWITCHES = (
52    'ParameterizedCommandLineFlags$Switches')
53_NATIVE_CRASH_RE = re.compile('(process|native) crash', re.IGNORECASE)
54_PICKLE_FORMAT_VERSION = 12
55
56# The ID of the bundle value Instrumentation uses to report which test index the
57# results are for in a collection of tests. Note that this index is 1-based.
58_BUNDLE_CURRENT_ID = 'current'
59# The ID of the bundle value Instrumentation uses to report the test class.
60_BUNDLE_CLASS_ID = 'class'
61# The ID of the bundle value Instrumentation uses to report the test name.
62_BUNDLE_TEST_ID = 'test'
63# The ID of the bundle value Instrumentation uses to report if a test was
64# skipped.
65_BUNDLE_SKIPPED_ID = 'test_skipped'
66# The ID of the bundle value Instrumentation uses to report the crash stack, if
67# the test crashed.
68_BUNDLE_STACK_ID = 'stack'
69
70# The ID of the bundle value Chrome uses to report the test duration.
71_BUNDLE_DURATION_ID = 'duration_ms'
72
73class MissingSizeAnnotationError(test_exception.TestException):
74  def __init__(self, class_name):
75    super(MissingSizeAnnotationError, self).__init__(class_name +
76        ': Test method is missing required size annotation. Add one of: ' +
77        ', '.join('@' + a for a in _VALID_ANNOTATIONS))
78
79
80class CommandLineParameterizationException(test_exception.TestException):
81
82  def __init__(self, msg):
83    super(CommandLineParameterizationException, self).__init__(msg)
84
85
86class TestListPickleException(test_exception.TestException):
87  pass
88
89
90# TODO(jbudorick): Make these private class methods of
91# InstrumentationTestInstance once the instrumentation junit3_runner_class is
92# deprecated.
93def ParseAmInstrumentRawOutput(raw_output):
94  """Parses the output of an |am instrument -r| call.
95
96  Args:
97    raw_output: the output of an |am instrument -r| call as a list of lines
98  Returns:
99    A 3-tuple containing:
100      - the instrumentation code as an integer
101      - the instrumentation result as a list of lines
102      - the instrumentation statuses received as a list of 2-tuples
103        containing:
104        - the status code as an integer
105        - the bundle dump as a dict mapping string keys to a list of
106          strings, one for each line.
107  """
108  parser = instrumentation_parser.InstrumentationParser(raw_output)
109  statuses = list(parser.IterStatus())
110  code, bundle = parser.GetResult()
111  return (code, bundle, statuses)
112
113
114def GenerateTestResults(result_code, result_bundle, statuses, duration_ms,
115                        device_abi, symbolizer):
116  """Generate test results from |statuses|.
117
118  Args:
119    result_code: The overall status code as an integer.
120    result_bundle: The summary bundle dump as a dict.
121    statuses: A list of 2-tuples containing:
122      - the status code as an integer
123      - the bundle dump as a dict mapping string keys to string values
124      Note that this is the same as the third item in the 3-tuple returned by
125      |_ParseAmInstrumentRawOutput|.
126    duration_ms: The duration of the test in milliseconds.
127    device_abi: The device_abi, which is needed for symbolization.
128    symbolizer: The symbolizer used to symbolize stack.
129
130  Returns:
131    A list containing an instance of InstrumentationTestResult for each test
132    parsed.
133  """
134
135  results = []
136
137  current_result = None
138  cumulative_duration = 0
139
140  for status_code, bundle in statuses:
141    # If the last test was a failure already, don't override that failure with
142    # post-test failures that could be caused by the original failure.
143    if (status_code == instrumentation_parser.STATUS_CODE_BATCH_FAILURE
144        and current_result.GetType() != base_test_result.ResultType.FAIL):
145      current_result.SetType(base_test_result.ResultType.FAIL)
146      _MaybeSetLog(bundle, current_result, symbolizer, device_abi)
147      continue
148
149    if status_code == instrumentation_parser.STATUS_CODE_TEST_DURATION:
150      # For the first result, duration will be set below to the difference
151      # between the reported and actual durations to account for overhead like
152      # starting instrumentation.
153      if results:
154        current_duration = int(bundle.get(_BUNDLE_DURATION_ID, duration_ms))
155        current_result.SetDuration(current_duration)
156        cumulative_duration += current_duration
157      continue
158
159    test_class = bundle.get(_BUNDLE_CLASS_ID, '')
160    test_method = bundle.get(_BUNDLE_TEST_ID, '')
161    if test_class and test_method:
162      test_name = '%s#%s' % (test_class, test_method)
163    else:
164      continue
165
166    if status_code == instrumentation_parser.STATUS_CODE_START:
167      if current_result:
168        results.append(current_result)
169      current_result = test_result.InstrumentationTestResult(
170          test_name, base_test_result.ResultType.UNKNOWN, duration_ms)
171    else:
172      if status_code == instrumentation_parser.STATUS_CODE_OK:
173        if bundle.get(_BUNDLE_SKIPPED_ID, '').lower() in ('true', '1', 'yes'):
174          current_result.SetType(base_test_result.ResultType.SKIP)
175        elif current_result.GetType() == base_test_result.ResultType.UNKNOWN:
176          current_result.SetType(base_test_result.ResultType.PASS)
177      elif status_code == instrumentation_parser.STATUS_CODE_SKIP:
178        current_result.SetType(base_test_result.ResultType.SKIP)
179      elif status_code == instrumentation_parser.STATUS_CODE_ASSUMPTION_FAILURE:
180        current_result.SetType(base_test_result.ResultType.SKIP)
181      else:
182        if status_code not in (instrumentation_parser.STATUS_CODE_ERROR,
183                               instrumentation_parser.STATUS_CODE_FAILURE):
184          logging.error('Unrecognized status code %d. Handling as an error.',
185                        status_code)
186        current_result.SetType(base_test_result.ResultType.FAIL)
187    _MaybeSetLog(bundle, current_result, symbolizer, device_abi)
188
189  if current_result:
190    if current_result.GetType() == base_test_result.ResultType.UNKNOWN:
191      crashed = (result_code == _ACTIVITY_RESULT_CANCELED and any(
192          _NATIVE_CRASH_RE.search(l) for l in six.itervalues(result_bundle)))
193      if crashed:
194        current_result.SetType(base_test_result.ResultType.CRASH)
195
196    results.append(current_result)
197
198  if results:
199    logging.info('Adding cumulative overhead to test %s: %dms',
200                 results[0].GetName(), duration_ms - cumulative_duration)
201    results[0].SetDuration(duration_ms - cumulative_duration)
202
203  return results
204
205
206def _MaybeSetLog(bundle, current_result, symbolizer, device_abi):
207  if _BUNDLE_STACK_ID in bundle:
208    stack = bundle[_BUNDLE_STACK_ID]
209    if symbolizer and device_abi:
210      current_result.SetLog('%s\n%s' % (stack, '\n'.join(
211          symbolizer.ExtractAndResolveNativeStackTraces(stack, device_abi))))
212    else:
213      current_result.SetLog(stack)
214
215    current_result.SetFailureReason(_ParseExceptionMessage(stack))
216
217
218def _ParseExceptionMessage(stack):
219  """Extracts the exception message from the given stack trace.
220  """
221  # This interprets stack traces reported via InstrumentationResultPrinter:
222  # https://source.chromium.org/chromium/chromium/src/+/main:third_party/android_support_test_runner/runner/src/main/java/android/support/test/internal/runner/listener/InstrumentationResultPrinter.java;l=181?q=InstrumentationResultPrinter&type=cs
223  # This is a standard Java stack trace, of the form:
224  # <Result of Exception.toString()>
225  #     at SomeClass.SomeMethod(...)
226  #     at ...
227  lines = stack.split('\n')
228  for i, line in enumerate(lines):
229    if line.startswith('\tat'):
230      return '\n'.join(lines[0:i])
231  # No call stack found, so assume everything is the exception message.
232  return stack
233
234
235def FilterTests(tests, filter_str=None, annotations=None,
236                excluded_annotations=None):
237  """Filter a list of tests
238
239  Args:
240    tests: a list of tests. e.g. [
241           {'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
242           {'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
243    filter_str: googletest-style filter string.
244    annotations: a dict of wanted annotations for test methods.
245    excluded_annotations: a dict of annotations to exclude.
246
247  Return:
248    A list of filtered tests
249  """
250
251  def test_names_from_pattern(combined_pattern, test_names):
252    patterns = combined_pattern.split(':')
253
254    hashable_patterns = set()
255    filename_patterns = []
256    for pattern in patterns:
257      if ('*' in pattern or '?' in pattern or '[' in pattern):
258        filename_patterns.append(pattern)
259      else:
260        hashable_patterns.add(pattern)
261
262    filter_test_names = set(
263        unittest_util.FilterTestNames(test_names, ':'.join(
264            filename_patterns))) if len(filename_patterns) > 0 else set()
265
266    for test_name in test_names:
267      if test_name in hashable_patterns:
268        filter_test_names.add(test_name)
269
270    return filter_test_names
271
272  def get_test_names(test):
273    test_names = set()
274    # Allow fully-qualified name as well as an omitted package.
275    unqualified_class_test = {
276        'class': test['class'].split('.')[-1],
277        'method': test['method']
278    }
279
280    test_name = GetTestName(test, sep='.')
281    test_names.add(test_name)
282
283    unqualified_class_test_name = GetTestName(unqualified_class_test, sep='.')
284    test_names.add(unqualified_class_test_name)
285
286    unique_test_name = GetUniqueTestName(test, sep='.')
287    test_names.add(unique_test_name)
288
289    if test['is_junit4']:
290      junit4_test_name = GetTestNameWithoutParameterPostfix(test, sep='.')
291      test_names.add(junit4_test_name)
292
293      unqualified_junit4_test_name = \
294        GetTestNameWithoutParameterPostfix(unqualified_class_test, sep='.')
295      test_names.add(unqualified_junit4_test_name)
296    return test_names
297
298  def get_tests_from_names(tests, test_names, tests_to_names):
299    ''' Returns the tests for which the given names apply
300
301    Args:
302      tests: a list of tests. e.g. [
303            {'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
304            {'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
305      test_names: a collection of names determining tests to return.
306
307    Return:
308      A list of tests that match the given test names
309    '''
310    filtered_tests = []
311    for t in tests:
312      current_test_names = tests_to_names[id(t)]
313
314      for current_test_name in current_test_names:
315        if current_test_name in test_names:
316          filtered_tests.append(t)
317          break
318
319    return filtered_tests
320
321  def remove_tests_from_names(tests, remove_test_names, tests_to_names):
322    ''' Returns the tests from the given list with given names removed
323
324    Args:
325      tests: a list of tests. e.g. [
326            {'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
327            {'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
328      remove_test_names: a collection of names determining tests to remove.
329      tests_to_names: a dcitionary of test ids to a collection of applicable
330            names for that test
331
332    Return:
333      A list of tests that don't match the given test names
334    '''
335    filtered_tests = []
336
337    for t in tests:
338      for name in tests_to_names[id(t)]:
339        if name in remove_test_names:
340          break
341      else:
342        filtered_tests.append(t)
343    return filtered_tests
344
345  def gtests_filter(tests, combined_filter):
346    ''' Returns the tests after the filter_str has been applied
347
348    Args:
349      tests: a list of tests. e.g. [
350            {'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
351            {'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
352      combined_filter: the filter string representing tests to exclude
353
354    Return:
355      A list of tests that should still be included after the filter_str is
356      applied to their names
357    '''
358
359    if not combined_filter:
360      return tests
361
362    # Collect all test names
363    all_test_names = set()
364    tests_to_names = {}
365    for t in tests:
366      tests_to_names[id(t)] = get_test_names(t)
367      for name in tests_to_names[id(t)]:
368        all_test_names.add(name)
369
370    pattern_groups = filter_str.split('-')
371    negative_pattern = pattern_groups[1] if len(pattern_groups) > 1 else None
372    positive_pattern = pattern_groups[0]
373
374    if positive_pattern:
375      # Only use the test names that match the positive pattern
376      positive_test_names = test_names_from_pattern(positive_pattern,
377                                                    all_test_names)
378      tests = get_tests_from_names(tests, positive_test_names, tests_to_names)
379
380    if negative_pattern:
381      # Remove any test the negative filter matches
382      remove_names = test_names_from_pattern(negative_pattern, all_test_names)
383      tests = remove_tests_from_names(tests, remove_names, tests_to_names)
384
385    return tests
386
387  def annotation_filter(all_annotations):
388    if not annotations:
389      return True
390    return any_annotation_matches(annotations, all_annotations)
391
392  def excluded_annotation_filter(all_annotations):
393    if not excluded_annotations:
394      return True
395    return not any_annotation_matches(excluded_annotations,
396                                      all_annotations)
397
398  def any_annotation_matches(filter_annotations, all_annotations):
399    return any(
400        ak in all_annotations
401        and annotation_value_matches(av, all_annotations[ak])
402        for ak, av in filter_annotations)
403
404  def annotation_value_matches(filter_av, av):
405    if filter_av is None:
406      return True
407    elif isinstance(av, dict):
408      tav_from_dict = av['value']
409      # If tav_from_dict is an int, the 'in' operator breaks, so convert
410      # filter_av and manually compare. See https://crbug.com/1019707
411      if isinstance(tav_from_dict, int):
412        return int(filter_av) == tav_from_dict
413      else:
414        return filter_av in tav_from_dict
415    elif isinstance(av, list):
416      return filter_av in av
417    return filter_av == av
418
419  return_tests = []
420  for t in gtests_filter(tests, filter_str):
421    # Enforce that all tests declare their size.
422    if not any(a in _VALID_ANNOTATIONS for a in t['annotations']):
423      raise MissingSizeAnnotationError(GetTestName(t))
424
425    if (not annotation_filter(t['annotations'])
426        or not excluded_annotation_filter(t['annotations'])):
427      continue
428    return_tests.append(t)
429
430  return return_tests
431
432# TODO(yolandyan): remove this once the tests are converted to junit4
433def GetAllTestsFromJar(test_jar):
434  pickle_path = '%s-proguard.pickle' % test_jar
435  try:
436    tests = GetTestsFromPickle(pickle_path, os.path.getmtime(test_jar))
437  except TestListPickleException as e:
438    logging.info('Could not get tests from pickle: %s', e)
439    logging.info('Getting tests from JAR via proguard.')
440    tests = _GetTestsFromProguard(test_jar)
441    SaveTestsToPickle(pickle_path, tests)
442  return tests
443
444
445def GetAllTestsFromApk(test_apk):
446  pickle_path = '%s-dexdump.pickle' % test_apk
447  try:
448    tests = GetTestsFromPickle(pickle_path, os.path.getmtime(test_apk))
449  except TestListPickleException as e:
450    logging.info('Could not get tests from pickle: %s', e)
451    logging.info('Getting tests from dex via dexdump.')
452    tests = _GetTestsFromDexdump(test_apk)
453    SaveTestsToPickle(pickle_path, tests)
454  return tests
455
456def GetTestsFromPickle(pickle_path, test_mtime):
457  if not os.path.exists(pickle_path):
458    raise TestListPickleException('%s does not exist.' % pickle_path)
459  if os.path.getmtime(pickle_path) <= test_mtime:
460    raise TestListPickleException('File is stale: %s' % pickle_path)
461
462  with open(pickle_path, 'r') as f:
463    pickle_data = pickle.load(f)
464  if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
465    raise TestListPickleException('PICKLE_FORMAT_VERSION has changed.')
466  return pickle_data['TEST_METHODS']
467
468
469# TODO(yolandyan): remove this once the test listing from java runner lands
470@instrumentation_tracing.no_tracing
471def _GetTestsFromProguard(jar_path):
472  p = proguard.Dump(jar_path)
473  class_lookup = dict((c['class'], c) for c in p['classes'])
474
475  def is_test_class(c):
476    return c['class'].endswith('Test')
477
478  def is_test_method(m):
479    return m['method'].startswith('test')
480
481  def recursive_class_annotations(c):
482    s = c['superclass']
483    if s in class_lookup:
484      a = recursive_class_annotations(class_lookup[s])
485    else:
486      a = {}
487    a.update(c['annotations'])
488    return a
489
490  def stripped_test_class(c):
491    return {
492      'class': c['class'],
493      'annotations': recursive_class_annotations(c),
494      'methods': [m for m in c['methods'] if is_test_method(m)],
495      'superclass': c['superclass'],
496    }
497
498  return [stripped_test_class(c) for c in p['classes']
499          if is_test_class(c)]
500
501
502def _GetTestsFromDexdump(test_apk):
503  dex_dumps = dexdump.Dump(test_apk)
504  tests = []
505
506  def get_test_methods(methods):
507    return [
508        {
509          'method': m,
510          # No annotation info is available from dexdump.
511          # Set MediumTest annotation for default.
512          'annotations': {'MediumTest': None},
513        } for m in methods if m.startswith('test')]
514
515  for dump in dex_dumps:
516    for package_name, package_info in six.iteritems(dump):
517      for class_name, class_info in six.iteritems(package_info['classes']):
518        if class_name.endswith('Test'):
519          tests.append({
520              'class': '%s.%s' % (package_name, class_name),
521              'annotations': {},
522              'methods': get_test_methods(class_info['methods']),
523              'superclass': class_info['superclass'],
524          })
525  return tests
526
527def SaveTestsToPickle(pickle_path, tests):
528  pickle_data = {
529    'VERSION': _PICKLE_FORMAT_VERSION,
530    'TEST_METHODS': tests,
531  }
532  with open(pickle_path, 'wb') as pickle_file:
533    pickle.dump(pickle_data, pickle_file)
534
535
536class MissingJUnit4RunnerException(test_exception.TestException):
537  """Raised when JUnit4 runner is not provided or specified in apk manifest"""
538
539  def __init__(self):
540    super(MissingJUnit4RunnerException, self).__init__(
541        'JUnit4 runner is not provided or specified in test apk manifest.')
542
543
544def GetTestName(test, sep='#'):
545  """Gets the name of the given test.
546
547  Note that this may return the same name for more than one test, e.g. if a
548  test is being run multiple times with different parameters.
549
550  Args:
551    test: the instrumentation test dict.
552    sep: the character(s) that should join the class name and the method name.
553  Returns:
554    The test name as a string.
555  """
556  test_name = '%s%s%s' % (test['class'], sep, test['method'])
557  assert ' *-:' not in test_name, (
558      'The test name must not contain any of the characters in " *-:". See '
559      'https://crbug.com/912199')
560  return test_name
561
562
563def GetTestNameWithoutParameterPostfix(
564      test, sep='#', parameterization_sep='__'):
565  """Gets the name of the given JUnit4 test without parameter postfix.
566
567  For most WebView JUnit4 javatests, each test is parameterizatized with
568  "__sandboxed_mode" to run in both non-sandboxed mode and sandboxed mode.
569
570  This function returns the name of the test without parameterization
571  so test filters can match both parameterized and non-parameterized tests.
572
573  Args:
574    test: the instrumentation test dict.
575    sep: the character(s) that should join the class name and the method name.
576    parameterization_sep: the character(s) that seperate method name and method
577                          parameterization postfix.
578  Returns:
579    The test name without parameter postfix as a string.
580  """
581  name = GetTestName(test, sep=sep)
582  return name.split(parameterization_sep)[0]
583
584
585def GetUniqueTestName(test, sep='#'):
586  """Gets the unique name of the given test.
587
588  This will include text to disambiguate between tests for which GetTestName
589  would return the same name.
590
591  Args:
592    test: the instrumentation test dict.
593    sep: the character(s) that should join the class name and the method name.
594  Returns:
595    The unique test name as a string.
596  """
597  display_name = GetTestName(test, sep=sep)
598  if test.get('flags', [None])[0]:
599    sanitized_flags = [x.replace('-', '_') for x in test['flags']]
600    display_name = '%s_with_%s' % (display_name, '_'.join(sanitized_flags))
601
602  assert ' *-:' not in display_name, (
603      'The test name must not contain any of the characters in " *-:". See '
604      'https://crbug.com/912199')
605
606  return display_name
607
608
609class InstrumentationTestInstance(test_instance.TestInstance):
610
611  def __init__(self, args, data_deps_delegate, error_func):
612    super(InstrumentationTestInstance, self).__init__()
613
614    self._additional_apks = []
615    self._apk_under_test = None
616    self._apk_under_test_incremental_install_json = None
617    self._modules = None
618    self._fake_modules = None
619    self._additional_locales = None
620    self._package_info = None
621    self._suite = None
622    self._test_apk = None
623    self._test_apk_incremental_install_json = None
624    self._test_jar = None
625    self._test_package = None
626    self._junit3_runner_class = None
627    self._junit4_runner_class = None
628    self._junit4_runner_supports_listing = None
629    self._test_support_apk = None
630    self._initializeApkAttributes(args, error_func)
631
632    self._data_deps = None
633    self._data_deps_delegate = None
634    self._runtime_deps_path = None
635    self._initializeDataDependencyAttributes(args, data_deps_delegate)
636
637    self._annotations = None
638    self._excluded_annotations = None
639    self._test_filter = None
640    self._initializeTestFilterAttributes(args)
641
642    self._flags = None
643    self._use_apk_under_test_flags_file = False
644    self._initializeFlagAttributes(args)
645
646    self._screenshot_dir = None
647    self._timeout_scale = None
648    self._wait_for_java_debugger = None
649    self._initializeTestControlAttributes(args)
650
651    self._coverage_directory = None
652    self._initializeTestCoverageAttributes(args)
653
654    self._store_tombstones = False
655    self._symbolizer = None
656    self._enable_breakpad_dump = False
657    self._enable_java_deobfuscation = False
658    self._deobfuscator = None
659    self._initializeLogAttributes(args)
660
661    self._edit_shared_prefs = []
662    self._initializeEditPrefsAttributes(args)
663
664    self._replace_system_package = None
665    self._initializeReplaceSystemPackageAttributes(args)
666
667    self._system_packages_to_remove = None
668    self._initializeSystemPackagesToRemoveAttributes(args)
669
670    self._use_webview_provider = None
671    self._initializeUseWebviewProviderAttributes(args)
672
673    self._skia_gold_properties = None
674    self._initializeSkiaGoldAttributes(args)
675
676    self._test_launcher_batch_limit = None
677    self._initializeTestLauncherAttributes(args)
678
679    self._wpr_enable_record = args.wpr_enable_record
680
681    self._external_shard_index = args.test_launcher_shard_index
682    self._total_external_shards = args.test_launcher_total_shards
683
684  def _initializeApkAttributes(self, args, error_func):
685    if args.apk_under_test:
686      apk_under_test_path = args.apk_under_test
687      if (not args.apk_under_test.endswith('.apk')
688          and not args.apk_under_test.endswith('.apks')):
689        apk_under_test_path = os.path.join(
690            constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
691            '%s.apk' % args.apk_under_test)
692
693      # TODO(jbudorick): Move the realpath up to the argument parser once
694      # APK-by-name is no longer supported.
695      apk_under_test_path = os.path.realpath(apk_under_test_path)
696
697      if not os.path.exists(apk_under_test_path):
698        error_func('Unable to find APK under test: %s' % apk_under_test_path)
699
700      self._apk_under_test = apk_helper.ToHelper(apk_under_test_path)
701
702    test_apk_path = args.test_apk
703    if not os.path.exists(test_apk_path):
704      test_apk_path = os.path.join(
705          constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
706          '%s.apk' % args.test_apk)
707      # TODO(jbudorick): Move the realpath up to the argument parser once
708      # APK-by-name is no longer supported.
709      test_apk_path = os.path.realpath(test_apk_path)
710
711    if not os.path.exists(test_apk_path):
712      error_func('Unable to find test APK: %s' % test_apk_path)
713
714    self._test_apk = apk_helper.ToHelper(test_apk_path)
715    self._suite = os.path.splitext(os.path.basename(args.test_apk))[0]
716
717    self._apk_under_test_incremental_install_json = (
718        args.apk_under_test_incremental_install_json)
719    self._test_apk_incremental_install_json = (
720        args.test_apk_incremental_install_json)
721
722    if self._test_apk_incremental_install_json:
723      assert self._suite.endswith('_incremental')
724      self._suite = self._suite[:-len('_incremental')]
725
726    self._modules = args.modules
727    self._fake_modules = args.fake_modules
728    self._additional_locales = args.additional_locales
729
730    self._test_jar = args.test_jar
731    self._test_support_apk = apk_helper.ToHelper(os.path.join(
732        constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
733        '%sSupport.apk' % self._suite))
734
735    if not self._test_jar:
736      logging.warning('Test jar not specified. Test runner will not have '
737                      'Java annotation info available. May not handle test '
738                      'timeouts correctly.')
739    elif not os.path.exists(self._test_jar):
740      error_func('Unable to find test JAR: %s' % self._test_jar)
741
742    self._test_package = self._test_apk.GetPackageName()
743    all_instrumentations = self._test_apk.GetAllInstrumentations()
744    all_junit3_runner_classes = [
745        x for x in all_instrumentations if ('0xffffffff' in x.get(
746            'chromium-junit3', ''))]
747    all_junit4_runner_classes = [
748        x for x in all_instrumentations if ('0xffffffff' not in x.get(
749            'chromium-junit3', ''))]
750
751    if len(all_junit3_runner_classes) > 1:
752      logging.warning('This test apk has more than one JUnit3 instrumentation')
753    if len(all_junit4_runner_classes) > 1:
754      logging.warning('This test apk has more than one JUnit4 instrumentation')
755
756    self._junit3_runner_class = (
757      all_junit3_runner_classes[0]['android:name']
758      if all_junit3_runner_classes else self.test_apk.GetInstrumentationName())
759
760    self._junit4_runner_class = (
761      all_junit4_runner_classes[0]['android:name']
762      if all_junit4_runner_classes else None)
763
764    if self._junit4_runner_class:
765      if self._test_apk_incremental_install_json:
766        self._junit4_runner_supports_listing = next(
767            (True for x in self._test_apk.GetAllMetadata()
768             if 'real-instr' in x[0] and x[1] in _TEST_LIST_JUNIT4_RUNNERS),
769            False)
770      else:
771        self._junit4_runner_supports_listing = (
772            self._junit4_runner_class in _TEST_LIST_JUNIT4_RUNNERS)
773
774    self._package_info = None
775    if self._apk_under_test:
776      package_under_test = self._apk_under_test.GetPackageName()
777      for package_info in six.itervalues(constants.PACKAGE_INFO):
778        if package_under_test == package_info.package:
779          self._package_info = package_info
780          break
781    if not self._package_info:
782      logging.warning(("Unable to find package info for %s. " +
783                       "(This may just mean that the test package is " +
784                       "currently being installed.)"),
785                       self._test_package)
786
787    for apk in args.additional_apks:
788      if not os.path.exists(apk):
789        error_func('Unable to find additional APK: %s' % apk)
790    self._additional_apks = (
791        [apk_helper.ToHelper(x) for x in args.additional_apks])
792
793  def _initializeDataDependencyAttributes(self, args, data_deps_delegate):
794    self._data_deps = []
795    self._data_deps_delegate = data_deps_delegate
796    self._runtime_deps_path = args.runtime_deps_path
797
798    if not self._runtime_deps_path:
799      logging.warning('No data dependencies will be pushed.')
800
801  def _initializeTestFilterAttributes(self, args):
802    self._test_filter = test_filter.InitializeFilterFromArgs(args)
803
804    def annotation_element(a):
805      a = a.split('=', 1)
806      return (a[0], a[1] if len(a) == 2 else None)
807
808    if args.annotation_str:
809      self._annotations = [
810          annotation_element(a) for a in args.annotation_str.split(',')]
811    elif not self._test_filter:
812      self._annotations = [
813          annotation_element(a) for a in _DEFAULT_ANNOTATIONS]
814    else:
815      self._annotations = []
816
817    if args.exclude_annotation_str:
818      self._excluded_annotations = [
819          annotation_element(a) for a in args.exclude_annotation_str.split(',')]
820    else:
821      self._excluded_annotations = []
822
823    requested_annotations = set(a[0] for a in self._annotations)
824    if not args.run_disabled:
825      self._excluded_annotations.extend(
826          annotation_element(a) for a in _EXCLUDE_UNLESS_REQUESTED_ANNOTATIONS
827          if a not in requested_annotations)
828
829  def _initializeFlagAttributes(self, args):
830    self._use_apk_under_test_flags_file = args.use_apk_under_test_flags_file
831    self._flags = ['--enable-test-intents']
832    if args.command_line_flags:
833      self._flags.extend(args.command_line_flags)
834    if args.device_flags_file:
835      with open(args.device_flags_file) as device_flags_file:
836        stripped_lines = (l.strip() for l in device_flags_file)
837        self._flags.extend(flag for flag in stripped_lines if flag)
838    if args.strict_mode and args.strict_mode != 'off' and (
839        # TODO(yliuyliu): Turn on strict mode for coverage once
840        # crbug/1006397 is fixed.
841        not args.coverage_dir):
842      self._flags.append('--strict-mode=' + args.strict_mode)
843
844  def _initializeTestControlAttributes(self, args):
845    self._screenshot_dir = args.screenshot_dir
846    self._timeout_scale = args.timeout_scale or 1
847    self._wait_for_java_debugger = args.wait_for_java_debugger
848
849  def _initializeTestCoverageAttributes(self, args):
850    self._coverage_directory = args.coverage_dir
851
852  def _initializeLogAttributes(self, args):
853    self._enable_breakpad_dump = args.enable_breakpad_dump
854    self._enable_java_deobfuscation = args.enable_java_deobfuscation
855    self._store_tombstones = args.store_tombstones
856    self._symbolizer = stack_symbolizer.Symbolizer(
857        self.apk_under_test.path if self.apk_under_test else None)
858
859  def _initializeEditPrefsAttributes(self, args):
860    if not hasattr(args, 'shared_prefs_file') or not args.shared_prefs_file:
861      return
862    if not isinstance(args.shared_prefs_file, str):
863      logging.warning("Given non-string for a filepath")
864      return
865    self._edit_shared_prefs = shared_preference_utils.ExtractSettingsFromJson(
866        args.shared_prefs_file)
867
868  def _initializeReplaceSystemPackageAttributes(self, args):
869    if (not hasattr(args, 'replace_system_package')
870        or not args.replace_system_package):
871      return
872    self._replace_system_package = args.replace_system_package
873
874  def _initializeSystemPackagesToRemoveAttributes(self, args):
875    if (not hasattr(args, 'system_packages_to_remove')
876        or not args.system_packages_to_remove):
877      return
878    self._system_packages_to_remove = args.system_packages_to_remove
879
880  def _initializeUseWebviewProviderAttributes(self, args):
881    if (not hasattr(args, 'use_webview_provider')
882        or not args.use_webview_provider):
883      return
884    self._use_webview_provider = args.use_webview_provider
885
886  def _initializeSkiaGoldAttributes(self, args):
887    self._skia_gold_properties = gold_utils.AndroidSkiaGoldProperties(args)
888
889  def _initializeTestLauncherAttributes(self, args):
890    if hasattr(args, 'test_launcher_batch_limit'):
891      self._test_launcher_batch_limit = args.test_launcher_batch_limit
892
893  @property
894  def additional_apks(self):
895    return self._additional_apks
896
897  @property
898  def apk_under_test(self):
899    return self._apk_under_test
900
901  @property
902  def apk_under_test_incremental_install_json(self):
903    return self._apk_under_test_incremental_install_json
904
905  @property
906  def modules(self):
907    return self._modules
908
909  @property
910  def fake_modules(self):
911    return self._fake_modules
912
913  @property
914  def additional_locales(self):
915    return self._additional_locales
916
917  @property
918  def coverage_directory(self):
919    return self._coverage_directory
920
921  @property
922  def edit_shared_prefs(self):
923    return self._edit_shared_prefs
924
925  @property
926  def enable_breakpad_dump(self):
927    return self._enable_breakpad_dump
928
929  @property
930  def external_shard_index(self):
931    return self._external_shard_index
932
933  @property
934  def flags(self):
935    return self._flags
936
937  @property
938  def junit3_runner_class(self):
939    return self._junit3_runner_class
940
941  @property
942  def junit4_runner_class(self):
943    return self._junit4_runner_class
944
945  @property
946  def junit4_runner_supports_listing(self):
947    return self._junit4_runner_supports_listing
948
949  @property
950  def package_info(self):
951    return self._package_info
952
953  @property
954  def replace_system_package(self):
955    return self._replace_system_package
956
957  @property
958  def use_webview_provider(self):
959    return self._use_webview_provider
960
961  @property
962  def screenshot_dir(self):
963    return self._screenshot_dir
964
965  @property
966  def skia_gold_properties(self):
967    return self._skia_gold_properties
968
969  @property
970  def store_tombstones(self):
971    return self._store_tombstones
972
973  @property
974  def suite(self):
975    return self._suite
976
977  @property
978  def symbolizer(self):
979    return self._symbolizer
980
981  @property
982  def system_packages_to_remove(self):
983    return self._system_packages_to_remove
984
985  @property
986  def test_apk(self):
987    return self._test_apk
988
989  @property
990  def test_apk_incremental_install_json(self):
991    return self._test_apk_incremental_install_json
992
993  @property
994  def test_filter(self):
995    return self._test_filter
996
997  @property
998  def test_jar(self):
999    return self._test_jar
1000
1001  @property
1002  def test_launcher_batch_limit(self):
1003    return self._test_launcher_batch_limit
1004
1005  @property
1006  def test_support_apk(self):
1007    return self._test_support_apk
1008
1009  @property
1010  def test_package(self):
1011    return self._test_package
1012
1013  @property
1014  def timeout_scale(self):
1015    return self._timeout_scale
1016
1017  @property
1018  def total_external_shards(self):
1019    return self._total_external_shards
1020
1021  @property
1022  def use_apk_under_test_flags_file(self):
1023    return self._use_apk_under_test_flags_file
1024
1025  @property
1026  def wait_for_java_debugger(self):
1027    return self._wait_for_java_debugger
1028
1029  @property
1030  def wpr_record_mode(self):
1031    return self._wpr_enable_record
1032
1033  @property
1034  def wpr_replay_mode(self):
1035    return not self._wpr_enable_record
1036
1037  #override
1038  def TestType(self):
1039    return 'instrumentation'
1040
1041  #override
1042  def GetPreferredAbis(self):
1043    # We could alternatively take the intersection of what they all support,
1044    # but it should never be the case that they support different things.
1045    apks = [self._test_apk, self._apk_under_test] + self._additional_apks
1046    for apk in apks:
1047      if apk:
1048        ret = apk.GetAbis()
1049        if ret:
1050          return ret
1051    return []
1052
1053  #override
1054  def SetUp(self):
1055    self._data_deps.extend(
1056        self._data_deps_delegate(self._runtime_deps_path))
1057    if self._enable_java_deobfuscation:
1058      self._deobfuscator = deobfuscator.DeobfuscatorPool(
1059          self.test_apk.path + '.mapping')
1060
1061  def GetDataDependencies(self):
1062    return self._data_deps
1063
1064  def GetTests(self):
1065    if self.test_jar:
1066      raw_tests = GetAllTestsFromJar(self.test_jar)
1067    else:
1068      raw_tests = GetAllTestsFromApk(self.test_apk.path)
1069    return self.ProcessRawTests(raw_tests)
1070
1071  def MaybeDeobfuscateLines(self, lines):
1072    if not self._deobfuscator:
1073      return lines
1074    return self._deobfuscator.TransformLines(lines)
1075
1076  def ProcessRawTests(self, raw_tests):
1077    inflated_tests = self._ParameterizeTestsWithFlags(
1078        self._InflateTests(raw_tests))
1079    if self._junit4_runner_class is None and any(
1080        t['is_junit4'] for t in inflated_tests):
1081      raise MissingJUnit4RunnerException()
1082    filtered_tests = FilterTests(
1083        inflated_tests, self._test_filter, self._annotations,
1084        self._excluded_annotations)
1085    if self._test_filter and not filtered_tests:
1086      for t in inflated_tests:
1087        logging.debug('  %s', GetUniqueTestName(t))
1088      logging.warning('Unmatched Filter: %s', self._test_filter)
1089    return filtered_tests
1090
1091  # pylint: disable=no-self-use
1092  def _InflateTests(self, tests):
1093    inflated_tests = []
1094    for c in tests:
1095      for m in c['methods']:
1096        a = dict(c['annotations'])
1097        a.update(m['annotations'])
1098        inflated_tests.append({
1099            'class': c['class'],
1100            'method': m['method'],
1101            'annotations': a,
1102            # TODO(https://crbug.com/1084729): Remove is_junit4.
1103            'is_junit4': True
1104        })
1105    return inflated_tests
1106
1107  def _ParameterizeTestsWithFlags(self, tests):
1108
1109    def _checkParameterization(annotations):
1110      types = [
1111          _PARAMETERIZED_COMMAND_LINE_FLAGS_SWITCHES,
1112          _PARAMETERIZED_COMMAND_LINE_FLAGS,
1113      ]
1114      if types[0] in annotations and types[1] in annotations:
1115        raise CommandLineParameterizationException(
1116            'Multiple command-line parameterization types: {}.'.format(
1117                ', '.join(types)))
1118
1119    def _switchesToFlags(switches):
1120      return ['--{}'.format(s) for s in switches if s]
1121
1122    def _annotationToSwitches(clazz, methods):
1123      if clazz == _PARAMETERIZED_COMMAND_LINE_FLAGS_SWITCHES:
1124        return [methods['value']]
1125      elif clazz == _PARAMETERIZED_COMMAND_LINE_FLAGS:
1126        list_of_switches = []
1127        for annotation in methods['value']:
1128          for clazz, methods in six.iteritems(annotation):
1129            list_of_switches += _annotationToSwitches(clazz, methods)
1130        return list_of_switches
1131      else:
1132        return []
1133
1134    def _setTestFlags(test, flags):
1135      if flags:
1136        test['flags'] = flags
1137      elif 'flags' in test:
1138        del test['flags']
1139
1140    new_tests = []
1141    for t in tests:
1142      annotations = t['annotations']
1143      list_of_switches = []
1144      _checkParameterization(annotations)
1145      if _SKIP_PARAMETERIZATION not in annotations:
1146        for clazz, methods in six.iteritems(annotations):
1147          list_of_switches += _annotationToSwitches(clazz, methods)
1148      if list_of_switches:
1149        _setTestFlags(t, _switchesToFlags(list_of_switches[0]))
1150        for p in list_of_switches[1:]:
1151          parameterized_t = copy.copy(t)
1152          _setTestFlags(parameterized_t, _switchesToFlags(p))
1153          new_tests.append(parameterized_t)
1154    return tests + new_tests
1155
1156  @staticmethod
1157  def ParseAmInstrumentRawOutput(raw_output):
1158    return ParseAmInstrumentRawOutput(raw_output)
1159
1160  @staticmethod
1161  def GenerateTestResults(result_code, result_bundle, statuses, duration_ms,
1162                          device_abi, symbolizer):
1163    return GenerateTestResults(result_code, result_bundle, statuses,
1164                               duration_ms, device_abi, symbolizer)
1165
1166  #override
1167  def TearDown(self):
1168    self.symbolizer.CleanUp()
1169    if self._deobfuscator:
1170      self._deobfuscator.Close()
1171      self._deobfuscator = None
1172