PageRenderTime 96ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/build/android/pylib/instrumentation/instrumentation_test_instance.py

https://gitlab.com/0072016/Facebook-SDK-
Python | 644 lines | 525 code | 73 blank | 46 comment | 77 complexity | 10824cc75e136ceed60543e867e0cc88 MD5 | raw file
  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. import collections
  5. import copy
  6. import logging
  7. import os
  8. import pickle
  9. import re
  10. from devil.android import apk_helper
  11. from devil.android import md5sum
  12. from pylib import constants
  13. from pylib.base import base_test_result
  14. from pylib.base import test_instance
  15. from pylib.constants import host_paths
  16. from pylib.instrumentation import test_result
  17. from pylib.instrumentation import instrumentation_parser
  18. from pylib.utils import proguard
  19. with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
  20. import unittest_util # pylint: disable=import-error
  21. # Ref: http://developer.android.com/reference/android/app/Activity.html
  22. _ACTIVITY_RESULT_CANCELED = 0
  23. _ACTIVITY_RESULT_OK = -1
  24. _COMMAND_LINE_PARAMETER = 'cmdlinearg-parameter'
  25. _DEFAULT_ANNOTATIONS = [
  26. 'Smoke', 'SmallTest', 'MediumTest', 'LargeTest',
  27. 'EnormousTest', 'IntegrationTest']
  28. _EXCLUDE_UNLESS_REQUESTED_ANNOTATIONS = [
  29. 'DisabledTest', 'FlakyTest']
  30. _EXTRA_DRIVER_TEST_LIST = (
  31. 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TestList')
  32. _EXTRA_DRIVER_TEST_LIST_FILE = (
  33. 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TestListFile')
  34. _EXTRA_DRIVER_TARGET_PACKAGE = (
  35. 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage')
  36. _EXTRA_DRIVER_TARGET_CLASS = (
  37. 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass')
  38. _EXTRA_TIMEOUT_SCALE = (
  39. 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TimeoutScale')
  40. _PARAMETERIZED_TEST_ANNOTATION = 'ParameterizedTest'
  41. _PARAMETERIZED_TEST_SET_ANNOTATION = 'ParameterizedTest$Set'
  42. _NATIVE_CRASH_RE = re.compile('native crash', re.IGNORECASE)
  43. _PICKLE_FORMAT_VERSION = 10
  44. # TODO(jbudorick): Make these private class methods of
  45. # InstrumentationTestInstance once the instrumentation test_runner is
  46. # deprecated.
  47. def ParseAmInstrumentRawOutput(raw_output):
  48. """Parses the output of an |am instrument -r| call.
  49. Args:
  50. raw_output: the output of an |am instrument -r| call as a list of lines
  51. Returns:
  52. A 3-tuple containing:
  53. - the instrumentation code as an integer
  54. - the instrumentation result as a list of lines
  55. - the instrumentation statuses received as a list of 2-tuples
  56. containing:
  57. - the status code as an integer
  58. - the bundle dump as a dict mapping string keys to a list of
  59. strings, one for each line.
  60. """
  61. parser = instrumentation_parser.InstrumentationParser(raw_output)
  62. statuses = list(parser.IterStatus())
  63. code, bundle = parser.GetResult()
  64. return (code, bundle, statuses)
  65. def GenerateTestResults(
  66. result_code, result_bundle, statuses, start_ms, duration_ms):
  67. """Generate test results from |statuses|.
  68. Args:
  69. result_code: The overall status code as an integer.
  70. result_bundle: The summary bundle dump as a dict.
  71. statuses: A list of 2-tuples containing:
  72. - the status code as an integer
  73. - the bundle dump as a dict mapping string keys to string values
  74. Note that this is the same as the third item in the 3-tuple returned by
  75. |_ParseAmInstrumentRawOutput|.
  76. start_ms: The start time of the test in milliseconds.
  77. duration_ms: The duration of the test in milliseconds.
  78. Returns:
  79. A list containing an instance of InstrumentationTestResult for each test
  80. parsed.
  81. """
  82. results = []
  83. current_result = None
  84. for status_code, bundle in statuses:
  85. test_class = bundle.get('class', '')
  86. test_method = bundle.get('test', '')
  87. if test_class and test_method:
  88. test_name = '%s#%s' % (test_class, test_method)
  89. else:
  90. continue
  91. if status_code == instrumentation_parser.STATUS_CODE_START:
  92. if current_result:
  93. results.append(current_result)
  94. current_result = test_result.InstrumentationTestResult(
  95. test_name, base_test_result.ResultType.UNKNOWN, start_ms, duration_ms)
  96. else:
  97. if status_code == instrumentation_parser.STATUS_CODE_OK:
  98. if bundle.get('test_skipped', '').lower() in ('true', '1', 'yes'):
  99. current_result.SetType(base_test_result.ResultType.SKIP)
  100. elif current_result.GetType() == base_test_result.ResultType.UNKNOWN:
  101. current_result.SetType(base_test_result.ResultType.PASS)
  102. else:
  103. if status_code not in (instrumentation_parser.STATUS_CODE_ERROR,
  104. instrumentation_parser.STATUS_CODE_FAILURE):
  105. logging.error('Unrecognized status code %d. Handling as an error.',
  106. status_code)
  107. current_result.SetType(base_test_result.ResultType.FAIL)
  108. if 'stack' in bundle:
  109. current_result.SetLog(bundle['stack'])
  110. if current_result:
  111. if current_result.GetType() == base_test_result.ResultType.UNKNOWN:
  112. crashed = (result_code == _ACTIVITY_RESULT_CANCELED
  113. and any(_NATIVE_CRASH_RE.search(l)
  114. for l in result_bundle.itervalues()))
  115. if crashed:
  116. current_result.SetType(base_test_result.ResultType.CRASH)
  117. results.append(current_result)
  118. return results
  119. def ParseCommandLineFlagParameters(annotations):
  120. """Determines whether the test is parameterized to be run with different
  121. command-line flags.
  122. Args:
  123. annotations: The annotations of the test.
  124. Returns:
  125. If the test is parameterized, returns a list of named tuples
  126. with lists of flags, e.g.:
  127. [(add=['--flag-to-add']), (remove=['--flag-to-remove']), ()]
  128. That means, the test must be run three times, the first time with
  129. "--flag-to-add" added to command-line, the second time with
  130. "--flag-to-remove" to be removed from command-line, and the third time
  131. with default command-line args. If the same flag is listed both for adding
  132. and for removing, it is left unchanged.
  133. If the test is not parametrized, returns None.
  134. """
  135. ParamsTuple = collections.namedtuple('ParamsTuple', ['add', 'remove'])
  136. parameterized_tests = []
  137. if _PARAMETERIZED_TEST_SET_ANNOTATION in annotations:
  138. if annotations[_PARAMETERIZED_TEST_SET_ANNOTATION]:
  139. parameterized_tests = annotations[
  140. _PARAMETERIZED_TEST_SET_ANNOTATION].get('tests', [])
  141. elif _PARAMETERIZED_TEST_ANNOTATION in annotations:
  142. parameterized_tests = [annotations[_PARAMETERIZED_TEST_ANNOTATION]]
  143. else:
  144. return None
  145. result = []
  146. for pt in parameterized_tests:
  147. if not pt:
  148. continue
  149. for p in pt['parameters']:
  150. if p['tag'] == _COMMAND_LINE_PARAMETER:
  151. to_add = []
  152. to_remove = []
  153. for a in p.get('arguments', []):
  154. if a['name'] == 'add':
  155. to_add = ['--%s' % f for f in a['stringArray']]
  156. elif a['name'] == 'remove':
  157. to_remove = ['--%s' % f for f in a['stringArray']]
  158. result.append(ParamsTuple(to_add, to_remove))
  159. return result if result else None
  160. class InstrumentationTestInstance(test_instance.TestInstance):
  161. def __init__(self, args, isolate_delegate, error_func):
  162. super(InstrumentationTestInstance, self).__init__()
  163. self._additional_apks = []
  164. self._apk_under_test = None
  165. self._apk_under_test_incremental_install_script = None
  166. self._package_info = None
  167. self._suite = None
  168. self._test_apk = None
  169. self._test_apk_incremental_install_script = None
  170. self._test_jar = None
  171. self._test_package = None
  172. self._test_runner = None
  173. self._test_support_apk = None
  174. self._initializeApkAttributes(args, error_func)
  175. self._data_deps = None
  176. self._isolate_abs_path = None
  177. self._isolate_delegate = None
  178. self._isolated_abs_path = None
  179. self._test_data = None
  180. self._initializeDataDependencyAttributes(args, isolate_delegate)
  181. self._annotations = None
  182. self._excluded_annotations = None
  183. self._test_filter = None
  184. self._initializeTestFilterAttributes(args)
  185. self._flags = None
  186. self._initializeFlagAttributes(args)
  187. self._driver_apk = None
  188. self._driver_package = None
  189. self._driver_name = None
  190. self._initializeDriverAttributes()
  191. self._timeout_scale = None
  192. self._initializeTestControlAttributes(args)
  193. def _initializeApkAttributes(self, args, error_func):
  194. if args.apk_under_test:
  195. apk_under_test_path = args.apk_under_test
  196. if not args.apk_under_test.endswith('.apk'):
  197. apk_under_test_path = os.path.join(
  198. constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
  199. '%s.apk' % args.apk_under_test)
  200. if not os.path.exists(apk_under_test_path):
  201. error_func('Unable to find APK under test: %s' % apk_under_test_path)
  202. self._apk_under_test = apk_helper.ToHelper(apk_under_test_path)
  203. if args.test_apk.endswith('.apk'):
  204. self._suite = os.path.splitext(os.path.basename(args.test_apk))[0]
  205. self._test_apk = apk_helper.ToHelper(args.test_apk)
  206. else:
  207. self._suite = args.test_apk
  208. self._test_apk = apk_helper.ToHelper(os.path.join(
  209. constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
  210. '%s.apk' % args.test_apk))
  211. self._apk_under_test_incremental_install_script = (
  212. args.apk_under_test_incremental_install_script)
  213. self._test_apk_incremental_install_script = (
  214. args.test_apk_incremental_install_script)
  215. if self._test_apk_incremental_install_script:
  216. assert self._suite.endswith('_incremental')
  217. self._suite = self._suite[:-len('_incremental')]
  218. self._test_jar = os.path.join(
  219. constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
  220. '%s.jar' % self._suite)
  221. self._test_support_apk = apk_helper.ToHelper(os.path.join(
  222. constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
  223. '%sSupport.apk' % self._suite))
  224. if not os.path.exists(self._test_apk.path):
  225. error_func('Unable to find test APK: %s' % self._test_apk.path)
  226. if not os.path.exists(self._test_jar):
  227. error_func('Unable to find test JAR: %s' % self._test_jar)
  228. self._test_package = self._test_apk.GetPackageName()
  229. self._test_runner = self._test_apk.GetInstrumentationName()
  230. self._package_info = None
  231. for package_info in constants.PACKAGE_INFO.itervalues():
  232. if self._test_package == package_info.test_package:
  233. self._package_info = package_info
  234. if not self._package_info:
  235. logging.warning('Unable to find package info for %s', self._test_package)
  236. for apk in args.additional_apks:
  237. if not os.path.exists(apk):
  238. error_func('Unable to find additional APK: %s' % apk)
  239. self._additional_apks = (
  240. [apk_helper.ToHelper(x) for x in args.additional_apks])
  241. def _initializeDataDependencyAttributes(self, args, isolate_delegate):
  242. self._data_deps = []
  243. if args.isolate_file_path:
  244. if os.path.isabs(args.isolate_file_path):
  245. self._isolate_abs_path = args.isolate_file_path
  246. else:
  247. self._isolate_abs_path = os.path.join(
  248. constants.DIR_SOURCE_ROOT, args.isolate_file_path)
  249. self._isolate_delegate = isolate_delegate
  250. self._isolated_abs_path = os.path.join(
  251. constants.GetOutDirectory(), '%s.isolated' % self._test_package)
  252. else:
  253. self._isolate_delegate = None
  254. # TODO(jbudorick): Deprecate and remove --test-data once data dependencies
  255. # are fully converted to isolate.
  256. if args.test_data:
  257. logging.info('Data dependencies specified via --test-data')
  258. self._test_data = args.test_data
  259. else:
  260. self._test_data = None
  261. if not self._isolate_delegate and not self._test_data:
  262. logging.warning('No data dependencies will be pushed.')
  263. def _initializeTestFilterAttributes(self, args):
  264. if args.test_filter:
  265. self._test_filter = args.test_filter.replace('#', '.')
  266. def annotation_dict_element(a):
  267. a = a.split('=')
  268. return (a[0], a[1] if len(a) == 2 else None)
  269. if args.annotation_str:
  270. self._annotations = dict(
  271. annotation_dict_element(a)
  272. for a in args.annotation_str.split(','))
  273. elif not self._test_filter:
  274. self._annotations = dict(
  275. annotation_dict_element(a)
  276. for a in _DEFAULT_ANNOTATIONS)
  277. else:
  278. self._annotations = {}
  279. if args.exclude_annotation_str:
  280. self._excluded_annotations = dict(
  281. annotation_dict_element(a)
  282. for a in args.exclude_annotation_str.split(','))
  283. else:
  284. self._excluded_annotations = {}
  285. self._excluded_annotations.update(
  286. {
  287. a: None for a in _EXCLUDE_UNLESS_REQUESTED_ANNOTATIONS
  288. if a not in self._annotations
  289. })
  290. def _initializeFlagAttributes(self, args):
  291. self._flags = ['--enable-test-intents']
  292. # TODO(jbudorick): Transition "--device-flags" to "--device-flags-file"
  293. if hasattr(args, 'device_flags') and args.device_flags:
  294. with open(args.device_flags) as device_flags_file:
  295. stripped_lines = (l.strip() for l in device_flags_file)
  296. self._flags.extend([flag for flag in stripped_lines if flag])
  297. if hasattr(args, 'device_flags_file') and args.device_flags_file:
  298. with open(args.device_flags_file) as device_flags_file:
  299. stripped_lines = (l.strip() for l in device_flags_file)
  300. self._flags.extend([flag for flag in stripped_lines if flag])
  301. if (hasattr(args, 'strict_mode') and
  302. args.strict_mode and
  303. args.strict_mode != 'off'):
  304. self._flags.append('--strict-mode=' + args.strict_mode)
  305. def _initializeDriverAttributes(self):
  306. self._driver_apk = os.path.join(
  307. constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
  308. 'OnDeviceInstrumentationDriver.apk')
  309. if os.path.exists(self._driver_apk):
  310. driver_apk = apk_helper.ApkHelper(self._driver_apk)
  311. self._driver_package = driver_apk.GetPackageName()
  312. self._driver_name = driver_apk.GetInstrumentationName()
  313. else:
  314. self._driver_apk = None
  315. def _initializeTestControlAttributes(self, args):
  316. self._timeout_scale = args.timeout_scale or 1
  317. @property
  318. def additional_apks(self):
  319. return self._additional_apks
  320. @property
  321. def apk_under_test(self):
  322. return self._apk_under_test
  323. @property
  324. def apk_under_test_incremental_install_script(self):
  325. return self._apk_under_test_incremental_install_script
  326. @property
  327. def flags(self):
  328. return self._flags
  329. @property
  330. def driver_apk(self):
  331. return self._driver_apk
  332. @property
  333. def driver_package(self):
  334. return self._driver_package
  335. @property
  336. def driver_name(self):
  337. return self._driver_name
  338. @property
  339. def package_info(self):
  340. return self._package_info
  341. @property
  342. def suite(self):
  343. return self._suite
  344. @property
  345. def test_apk(self):
  346. return self._test_apk
  347. @property
  348. def test_apk_incremental_install_script(self):
  349. return self._test_apk_incremental_install_script
  350. @property
  351. def test_jar(self):
  352. return self._test_jar
  353. @property
  354. def test_support_apk(self):
  355. return self._test_support_apk
  356. @property
  357. def test_package(self):
  358. return self._test_package
  359. @property
  360. def test_runner(self):
  361. return self._test_runner
  362. @property
  363. def timeout_scale(self):
  364. return self._timeout_scale
  365. #override
  366. def TestType(self):
  367. return 'instrumentation'
  368. #override
  369. def SetUp(self):
  370. if self._isolate_delegate:
  371. self._isolate_delegate.Remap(
  372. self._isolate_abs_path, self._isolated_abs_path)
  373. self._isolate_delegate.MoveOutputDeps()
  374. self._data_deps.extend([(self._isolate_delegate.isolate_deps_dir, None)])
  375. # TODO(jbudorick): Convert existing tests that depend on the --test-data
  376. # mechanism to isolate, then remove this.
  377. if self._test_data:
  378. for t in self._test_data:
  379. device_rel_path, host_rel_path = t.split(':')
  380. host_abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, host_rel_path)
  381. self._data_deps.extend(
  382. [(host_abs_path,
  383. [None, 'chrome', 'test', 'data', device_rel_path])])
  384. def GetDataDependencies(self):
  385. return self._data_deps
  386. def GetTests(self):
  387. pickle_path = '%s-proguard.pickle' % self.test_jar
  388. try:
  389. tests = self._GetTestsFromPickle(pickle_path, self.test_jar)
  390. except self.ProguardPickleException as e:
  391. logging.info('Getting tests from JAR via proguard. (%s)', str(e))
  392. tests = self._GetTestsFromProguard(self.test_jar)
  393. self._SaveTestsToPickle(pickle_path, self.test_jar, tests)
  394. return self._ParametrizeTestsWithFlags(
  395. self._InflateTests(self._FilterTests(tests)))
  396. class ProguardPickleException(Exception):
  397. pass
  398. def _GetTestsFromPickle(self, pickle_path, jar_path):
  399. if not os.path.exists(pickle_path):
  400. raise self.ProguardPickleException('%s does not exist.' % pickle_path)
  401. if os.path.getmtime(pickle_path) <= os.path.getmtime(jar_path):
  402. raise self.ProguardPickleException(
  403. '%s newer than %s.' % (jar_path, pickle_path))
  404. with open(pickle_path, 'r') as pickle_file:
  405. pickle_data = pickle.loads(pickle_file.read())
  406. jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
  407. try:
  408. if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
  409. raise self.ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
  410. if pickle_data['JAR_MD5SUM'] != jar_md5:
  411. raise self.ProguardPickleException('JAR file MD5 sum differs.')
  412. return pickle_data['TEST_METHODS']
  413. except TypeError as e:
  414. logging.error(pickle_data)
  415. raise self.ProguardPickleException(str(e))
  416. # pylint: disable=no-self-use
  417. def _GetTestsFromProguard(self, jar_path):
  418. p = proguard.Dump(jar_path)
  419. def is_test_class(c):
  420. return c['class'].endswith('Test')
  421. def is_test_method(m):
  422. return m['method'].startswith('test')
  423. class_lookup = dict((c['class'], c) for c in p['classes'])
  424. def recursive_get_class_annotations(c):
  425. s = c['superclass']
  426. if s in class_lookup:
  427. a = recursive_get_class_annotations(class_lookup[s])
  428. else:
  429. a = {}
  430. a.update(c['annotations'])
  431. return a
  432. def stripped_test_class(c):
  433. return {
  434. 'class': c['class'],
  435. 'annotations': recursive_get_class_annotations(c),
  436. 'methods': [m for m in c['methods'] if is_test_method(m)],
  437. }
  438. return [stripped_test_class(c) for c in p['classes']
  439. if is_test_class(c)]
  440. def _SaveTestsToPickle(self, pickle_path, jar_path, tests):
  441. jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
  442. pickle_data = {
  443. 'VERSION': _PICKLE_FORMAT_VERSION,
  444. 'JAR_MD5SUM': jar_md5,
  445. 'TEST_METHODS': tests,
  446. }
  447. with open(pickle_path, 'w') as pickle_file:
  448. pickle.dump(pickle_data, pickle_file)
  449. def _FilterTests(self, tests):
  450. def gtest_filter(c, m):
  451. t = ['%s.%s' % (c['class'].split('.')[-1], m['method'])]
  452. return (not self._test_filter
  453. or unittest_util.FilterTestNames(t, self._test_filter))
  454. def annotation_filter(all_annotations):
  455. if not self._annotations:
  456. return True
  457. return any_annotation_matches(self._annotations, all_annotations)
  458. def excluded_annotation_filter(all_annotations):
  459. if not self._excluded_annotations:
  460. return True
  461. return not any_annotation_matches(self._excluded_annotations,
  462. all_annotations)
  463. def any_annotation_matches(annotations, all_annotations):
  464. return any(
  465. ak in all_annotations and (av is None or av == all_annotations[ak])
  466. for ak, av in annotations.iteritems())
  467. filtered_classes = []
  468. for c in tests:
  469. filtered_methods = []
  470. for m in c['methods']:
  471. # Gtest filtering
  472. if not gtest_filter(c, m):
  473. continue
  474. all_annotations = dict(c['annotations'])
  475. all_annotations.update(m['annotations'])
  476. if (not annotation_filter(all_annotations)
  477. or not excluded_annotation_filter(all_annotations)):
  478. continue
  479. filtered_methods.append(m)
  480. if filtered_methods:
  481. filtered_class = dict(c)
  482. filtered_class['methods'] = filtered_methods
  483. filtered_classes.append(filtered_class)
  484. return filtered_classes
  485. def _InflateTests(self, tests):
  486. inflated_tests = []
  487. for c in tests:
  488. for m in c['methods']:
  489. a = dict(c['annotations'])
  490. a.update(m['annotations'])
  491. inflated_tests.append({
  492. 'class': c['class'],
  493. 'method': m['method'],
  494. 'annotations': a,
  495. })
  496. return inflated_tests
  497. def _ParametrizeTestsWithFlags(self, tests):
  498. new_tests = []
  499. for t in tests:
  500. parameters = ParseCommandLineFlagParameters(t['annotations'])
  501. if parameters:
  502. t['flags'] = parameters[0]
  503. for p in parameters[1:]:
  504. parameterized_t = copy.copy(t)
  505. parameterized_t['flags'] = p
  506. new_tests.append(parameterized_t)
  507. return tests + new_tests
  508. def GetDriverEnvironmentVars(
  509. self, test_list=None, test_list_file_path=None):
  510. env = {
  511. _EXTRA_DRIVER_TARGET_PACKAGE: self.test_package,
  512. _EXTRA_DRIVER_TARGET_CLASS: self.test_runner,
  513. _EXTRA_TIMEOUT_SCALE: self._timeout_scale,
  514. }
  515. if test_list:
  516. env[_EXTRA_DRIVER_TEST_LIST] = ','.join(test_list)
  517. if test_list_file_path:
  518. env[_EXTRA_DRIVER_TEST_LIST_FILE] = (
  519. os.path.basename(test_list_file_path))
  520. return env
  521. @staticmethod
  522. def ParseAmInstrumentRawOutput(raw_output):
  523. return ParseAmInstrumentRawOutput(raw_output)
  524. @staticmethod
  525. def GenerateTestResults(
  526. result_code, result_bundle, statuses, start_ms, duration_ms):
  527. return GenerateTestResults(result_code, result_bundle, statuses,
  528. start_ms, duration_ms)
  529. #override
  530. def TearDown(self):
  531. if self._isolate_delegate:
  532. self._isolate_delegate.Clear()