Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(162)

Side by Side Diff: build/android/pylib/instrumentation/instrumentation_test_instance.py

Issue 1851143002: Find annotated tests by exposing API in instrumentation_test_instance (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Change after mikecase's comments Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2015 The Chromium Authors. All rights reserved. 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 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import collections 5 import collections
6 import copy 6 import copy
7 import logging 7 import logging
8 import os 8 import os
9 import pickle 9 import pickle
10 import re 10 import re
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
52 _PICKLE_FORMAT_VERSION = 10 52 _PICKLE_FORMAT_VERSION = 10
53 53
54 54
55 class MissingSizeAnnotationError(Exception): 55 class MissingSizeAnnotationError(Exception):
56 def __init__(self, class_name): 56 def __init__(self, class_name):
57 super(MissingSizeAnnotationError, self).__init__(class_name + 57 super(MissingSizeAnnotationError, self).__init__(class_name +
58 ': Test method is missing required size annotation. Add one of: ' + 58 ': Test method is missing required size annotation. Add one of: ' +
59 ', '.join('@' + a for a in _VALID_ANNOTATIONS)) 59 ', '.join('@' + a for a in _VALID_ANNOTATIONS))
60 60
61 61
62 class ProguardPickleException(Exception):
63 pass
64
65
62 # TODO(jbudorick): Make these private class methods of 66 # TODO(jbudorick): Make these private class methods of
63 # InstrumentationTestInstance once the instrumentation test_runner is 67 # InstrumentationTestInstance once the instrumentation test_runner is
64 # deprecated. 68 # deprecated.
65 def ParseAmInstrumentRawOutput(raw_output): 69 def ParseAmInstrumentRawOutput(raw_output):
66 """Parses the output of an |am instrument -r| call. 70 """Parses the output of an |am instrument -r| call.
67 71
68 Args: 72 Args:
69 raw_output: the output of an |am instrument -r| call as a list of lines 73 raw_output: the output of an |am instrument -r| call as a list of lines
70 Returns: 74 Returns:
71 A 3-tuple containing: 75 A 3-tuple containing:
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
191 to_remove = [] 195 to_remove = []
192 for a in p.get('arguments', []): 196 for a in p.get('arguments', []):
193 if a['name'] == 'add': 197 if a['name'] == 'add':
194 to_add = ['--%s' % f for f in a['stringArray']] 198 to_add = ['--%s' % f for f in a['stringArray']]
195 elif a['name'] == 'remove': 199 elif a['name'] == 'remove':
196 to_remove = ['--%s' % f for f in a['stringArray']] 200 to_remove = ['--%s' % f for f in a['stringArray']]
197 result.append(ParamsTuple(to_add, to_remove)) 201 result.append(ParamsTuple(to_add, to_remove))
198 return result if result else None 202 return result if result else None
199 203
200 204
205 def FilterTests(tests, test_filter=None, annotations=None,
206 excluded_annotations=None):
207 """Filter a list of tests
208
209 Args:
210 tests: a list of tests. e.g. [
211 {'annotations": {}, 'class': 'com.example.TestA', 'methods':[]},
212 {'annotations": {}, 'class': 'com.example.TestB', 'methods':[]}]
213 test_filter: googletest-style filter string.
214 annotations: a dict of wanted annotations for test methods.
215 exclude_annotations: a dict of annotations to exclude.
216
217 Return:
218 A list of filtered tests
219 """
220 def gtest_filter(c, m):
221 if not test_filter:
222 return True
223 # Allow fully-qualified name as well as an omitted package.
224 names = ['%s.%s' % (c['class'], m['method']),
225 '%s.%s' % (c['class'].split('.')[-1], m['method'])]
226 return unittest_util.FilterTestNames(names, test_filter)
227
228 def annotation_filter(all_annotations):
229 if not annotations:
230 return True
231 return any_annotation_matches(annotations, all_annotations)
232
233 def excluded_annotation_filter(all_annotations):
234 if not excluded_annotations:
235 return True
236 return not any_annotation_matches(excluded_annotations,
237 all_annotations)
238
239 def any_annotation_matches(annotations, all_annotations):
240 return any(
241 ak in all_annotations and (av is None or av == all_annotations[ak])
242 for ak, av in annotations.iteritems())
243
244 filtered_classes = []
245 for c in tests:
246 filtered_methods = []
247 for m in c['methods']:
248 # Gtest filtering
249 if not gtest_filter(c, m):
250 continue
251
252 all_annotations = dict(c['annotations'])
253 all_annotations.update(m['annotations'])
254
255 # Enforce that all tests declare their size.
256 if not any(a in _VALID_ANNOTATIONS for a in all_annotations):
257 raise MissingSizeAnnotationError('%s.%s' % (c['class'], m['method']))
258
259 if (not annotation_filter(all_annotations)
260 or not excluded_annotation_filter(all_annotations)):
261 continue
262
263 filtered_methods.append(m)
264
265 if filtered_methods:
266 filtered_class = dict(c)
267 filtered_class['methods'] = filtered_methods
268 filtered_classes.append(filtered_class)
269
270 return filtered_classes
271
272
273 def GetAllTests(test_jar):
274 pickle_path = '%s-proguard.pickle' % test_jar
275 try:
276 tests = _GetTestsFromPickle(pickle_path, test_jar)
277 except ProguardPickleException as e:
278 logging.info('Could not get tests from pickle: %s', e)
279 logging.info('Getting tests from JAR via proguard.')
280 tests = _GetTestsFromProguard(test_jar)
281 _SaveTestsToPickle(pickle_path, test_jar, tests)
282 return tests
283
284
285 def _GetTestsFromPickle(pickle_path, jar_path):
286 if not os.path.exists(pickle_path):
287 raise ProguardPickleException('%s does not exist.' % pickle_path)
288 if os.path.getmtime(pickle_path) <= os.path.getmtime(jar_path):
289 raise ProguardPickleException(
290 '%s newer than %s.' % (jar_path, pickle_path))
291
292 with open(pickle_path, 'r') as pickle_file:
293 pickle_data = pickle.loads(pickle_file.read())
294 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
295
296 if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
297 raise ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
298 if pickle_data['JAR_MD5SUM'] != jar_md5:
299 raise ProguardPickleException('JAR file MD5 sum differs.')
300 return pickle_data['TEST_METHODS']
301
jbudorick 2016/06/11 00:24:02 nit: +1 blank line
the real yoland 2016/06/11 01:35:43 Done
302 def _GetTestsFromProguard(jar_path):
303 p = proguard.Dump(jar_path)
304 class_lookup = dict((c['class'], c) for c in p['classes'])
305
306 def is_test_class(c):
307 return c['class'].endswith('Test')
308
309 def is_test_method(m):
310 return m['method'].startswith('test')
311
312 def recursive_class_annotations(c):
313 s = c['superclass']
314 if s in class_lookup:
315 a = recursive_class_annotations(class_lookup[s])
316 else:
317 a = {}
318 a.update(c['annotations'])
319 return a
320
321 def stripped_test_class(c):
322 return {
323 'class': c['class'],
324 'annotations': recursive_class_annotations(c),
325 'methods': [m for m in c['methods'] if is_test_method(m)],
326 }
327
328 return [stripped_test_class(c) for c in p['classes']
329 if is_test_class(c)]
330
jbudorick 2016/06/11 00:24:02 nit: +1 blank line
the real yoland 2016/06/11 01:35:43 Done
331 def _SaveTestsToPickle(pickle_path, jar_path, tests):
332 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
333 pickle_data = {
334 'VERSION': _PICKLE_FORMAT_VERSION,
335 'JAR_MD5SUM': jar_md5,
336 'TEST_METHODS': tests,
337 }
338 with open(pickle_path, 'w') as pickle_file:
339 pickle.dump(pickle_data, pickle_file)
340
jbudorick 2016/06/11 00:24:02 nit: +1 blank line
the real yoland 2016/06/11 01:35:43 Done
201 class InstrumentationTestInstance(test_instance.TestInstance): 341 class InstrumentationTestInstance(test_instance.TestInstance):
202 342
203 def __init__(self, args, isolate_delegate, error_func): 343 def __init__(self, args, isolate_delegate, error_func):
204 super(InstrumentationTestInstance, self).__init__() 344 super(InstrumentationTestInstance, self).__init__()
205 345
206 self._additional_apks = [] 346 self._additional_apks = []
207 self._apk_under_test = None 347 self._apk_under_test = None
208 self._apk_under_test_incremental_install_script = None 348 self._apk_under_test_incremental_install_script = None
209 self._package_info = None 349 self._package_info = None
210 self._suite = None 350 self._suite = None
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
287 427
288 self._test_package = self._test_apk.GetPackageName() 428 self._test_package = self._test_apk.GetPackageName()
289 self._test_runner = self._test_apk.GetInstrumentationName() 429 self._test_runner = self._test_apk.GetInstrumentationName()
290 430
291 self._package_info = None 431 self._package_info = None
292 if self._apk_under_test: 432 if self._apk_under_test:
293 package_under_test = self._apk_under_test.GetPackageName() 433 package_under_test = self._apk_under_test.GetPackageName()
294 for package_info in constants.PACKAGE_INFO.itervalues(): 434 for package_info in constants.PACKAGE_INFO.itervalues():
295 if package_under_test == package_info.package: 435 if package_under_test == package_info.package:
296 self._package_info = package_info 436 self._package_info = package_info
437 break
297 if not self._package_info: 438 if not self._package_info:
298 logging.warning('Unable to find package info for %s', self._test_package) 439 logging.warning('Unable to find package info for %s', self._test_package)
299 440
300 for apk in args.additional_apks: 441 for apk in args.additional_apks:
301 if not os.path.exists(apk): 442 if not os.path.exists(apk):
302 error_func('Unable to find additional APK: %s' % apk) 443 error_func('Unable to find additional APK: %s' % apk)
303 self._additional_apks = ( 444 self._additional_apks = (
304 [apk_helper.ToHelper(x) for x in args.additional_apks]) 445 [apk_helper.ToHelper(x) for x in args.additional_apks])
305 446
306 def _initializeDataDependencyAttributes(self, args, isolate_delegate): 447 def _initializeDataDependencyAttributes(self, args, isolate_delegate):
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
486 device_rel_path, host_rel_path = t.split(':') 627 device_rel_path, host_rel_path = t.split(':')
487 host_abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, host_rel_path) 628 host_abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, host_rel_path)
488 self._data_deps.extend( 629 self._data_deps.extend(
489 [(host_abs_path, 630 [(host_abs_path,
490 [None, 'chrome', 'test', 'data', device_rel_path])]) 631 [None, 'chrome', 'test', 'data', device_rel_path])])
491 632
492 def GetDataDependencies(self): 633 def GetDataDependencies(self):
493 return self._data_deps 634 return self._data_deps
494 635
495 def GetTests(self): 636 def GetTests(self):
496 pickle_path = '%s-proguard.pickle' % self.test_jar 637 tests = GetAllTests(self.test_jar)
497 try: 638 filtered_tests = FilterTests(
498 tests = self._GetTestsFromPickle(pickle_path, self.test_jar) 639 tests, self._test_filter, self._annotations, self._excluded_annotations)
499 except self.ProguardPickleException as e: 640 return self._ParametrizeTestsWithFlags(self._InflateTests(filtered_tests))
500 logging.info('Getting tests from JAR via proguard. (%s)', str(e))
501 tests = self._GetTestsFromProguard(self.test_jar)
502 self._SaveTestsToPickle(pickle_path, self.test_jar, tests)
503 return self._ParametrizeTestsWithFlags(
504 self._InflateTests(self._FilterTests(tests)))
505
506 class ProguardPickleException(Exception):
507 pass
508
509 def _GetTestsFromPickle(self, pickle_path, jar_path):
510 if not os.path.exists(pickle_path):
511 raise self.ProguardPickleException('%s does not exist.' % pickle_path)
512 if os.path.getmtime(pickle_path) <= os.path.getmtime(jar_path):
513 raise self.ProguardPickleException(
514 '%s newer than %s.' % (jar_path, pickle_path))
515
516 with open(pickle_path, 'r') as pickle_file:
517 pickle_data = pickle.loads(pickle_file.read())
518 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
519
520 try:
521 if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
522 raise self.ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
523 if pickle_data['JAR_MD5SUM'] != jar_md5:
524 raise self.ProguardPickleException('JAR file MD5 sum differs.')
525 return pickle_data['TEST_METHODS']
526 except TypeError as e:
527 logging.error(pickle_data)
528 raise self.ProguardPickleException(str(e))
529 641
530 # pylint: disable=no-self-use 642 # pylint: disable=no-self-use
531 def _GetTestsFromProguard(self, jar_path):
532 p = proguard.Dump(jar_path)
533
534 def is_test_class(c):
535 return c['class'].endswith('Test')
536
537 def is_test_method(m):
538 return m['method'].startswith('test')
539
540 class_lookup = dict((c['class'], c) for c in p['classes'])
541 def recursive_get_class_annotations(c):
542 s = c['superclass']
543 if s in class_lookup:
544 a = recursive_get_class_annotations(class_lookup[s])
545 else:
546 a = {}
547 a.update(c['annotations'])
548 return a
549
550 def stripped_test_class(c):
551 return {
552 'class': c['class'],
553 'annotations': recursive_get_class_annotations(c),
554 'methods': [m for m in c['methods'] if is_test_method(m)],
555 }
556
557 return [stripped_test_class(c) for c in p['classes']
558 if is_test_class(c)]
559
560 def _SaveTestsToPickle(self, pickle_path, jar_path, tests):
561 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
562 pickle_data = {
563 'VERSION': _PICKLE_FORMAT_VERSION,
564 'JAR_MD5SUM': jar_md5,
565 'TEST_METHODS': tests,
566 }
567 with open(pickle_path, 'w') as pickle_file:
568 pickle.dump(pickle_data, pickle_file)
569
570 def _FilterTests(self, tests):
571
572 def gtest_filter(c, m):
573 if not self._test_filter:
574 return True
575 # Allow fully-qualified name as well as an omitted package.
576 names = ['%s.%s' % (c['class'], m['method']),
577 '%s.%s' % (c['class'].split('.')[-1], m['method'])]
578 return unittest_util.FilterTestNames(names, self._test_filter)
579
580 def annotation_filter(all_annotations):
581 if not self._annotations:
582 return True
583 return any_annotation_matches(self._annotations, all_annotations)
584
585 def excluded_annotation_filter(all_annotations):
586 if not self._excluded_annotations:
587 return True
588 return not any_annotation_matches(self._excluded_annotations,
589 all_annotations)
590
591 def any_annotation_matches(annotations, all_annotations):
592 return any(
593 ak in all_annotations and (av is None or av == all_annotations[ak])
594 for ak, av in annotations.iteritems())
595
596 filtered_classes = []
597 for c in tests:
598 filtered_methods = []
599 for m in c['methods']:
600 # Gtest filtering
601 if not gtest_filter(c, m):
602 continue
603
604 all_annotations = dict(c['annotations'])
605 all_annotations.update(m['annotations'])
606
607 # Enforce that all tests declare their size.
608 if not any(a in _VALID_ANNOTATIONS for a in all_annotations):
609 raise MissingSizeAnnotationError('%s.%s' % (c['class'], m['method']))
610
611 if (not annotation_filter(all_annotations)
612 or not excluded_annotation_filter(all_annotations)):
613 continue
614
615 filtered_methods.append(m)
616
617 if filtered_methods:
618 filtered_class = dict(c)
619 filtered_class['methods'] = filtered_methods
620 filtered_classes.append(filtered_class)
621
622 return filtered_classes
623
624 def _InflateTests(self, tests): 643 def _InflateTests(self, tests):
625 inflated_tests = [] 644 inflated_tests = []
626 for c in tests: 645 for c in tests:
627 for m in c['methods']: 646 for m in c['methods']:
628 a = dict(c['annotations']) 647 a = dict(c['annotations'])
629 a.update(m['annotations']) 648 a.update(m['annotations'])
630 inflated_tests.append({ 649 inflated_tests.append({
631 'class': c['class'], 650 'class': c['class'],
632 'method': m['method'], 651 'method': m['method'],
633 'annotations': a, 652 'annotations': a,
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
670 @staticmethod 689 @staticmethod
671 def GenerateTestResults( 690 def GenerateTestResults(
672 result_code, result_bundle, statuses, start_ms, duration_ms): 691 result_code, result_bundle, statuses, start_ms, duration_ms):
673 return GenerateTestResults(result_code, result_bundle, statuses, 692 return GenerateTestResults(result_code, result_bundle, statuses,
674 start_ms, duration_ms) 693 start_ms, duration_ms)
675 694
676 #override 695 #override
677 def TearDown(self): 696 def TearDown(self):
678 if self._isolate_delegate: 697 if self._isolate_delegate:
679 self._isolate_delegate.Clear() 698 self._isolate_delegate.Clear()
680
OLDNEW
« no previous file with comments | « no previous file | tools/android/find_annotated_tests.py » ('j') | tools/android/find_annotated_tests.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698