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

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: Rebase Created 4 years, 5 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
« no previous file with comments | « no previous file | build/android/pylib/instrumentation/instrumentation_test_instance_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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(filter_annotations, all_annotations):
240 return any(
241 ak in all_annotations
242 and annotation_value_matches(av, all_annotations[ak])
243 for ak, av in filter_annotations.iteritems())
244
245 def annotation_value_matches(filter_av, av):
246 if filter_av is None:
247 return True
248 elif isinstance(av, dict):
249 return filter_av in av['value']
250 elif isinstance(av, list):
251 return filter_av in av
252 return filter_av == av
253
254 filtered_classes = []
255 for c in tests:
256 filtered_methods = []
257 for m in c['methods']:
258 # Gtest filtering
259 if not gtest_filter(c, m):
260 continue
261
262 all_annotations = dict(c['annotations'])
263 all_annotations.update(m['annotations'])
264
265 # Enforce that all tests declare their size.
266 if not any(a in _VALID_ANNOTATIONS for a in all_annotations):
267 raise MissingSizeAnnotationError('%s.%s' % (c['class'], m['method']))
268
269 if (not annotation_filter(all_annotations)
270 or not excluded_annotation_filter(all_annotations)):
271 continue
272
273 filtered_methods.append(m)
274
275 if filtered_methods:
276 filtered_class = dict(c)
277 filtered_class['methods'] = filtered_methods
278 filtered_classes.append(filtered_class)
279
280 return filtered_classes
281
282 def GetAllTests(test_jar):
283 pickle_path = '%s-proguard.pickle' % test_jar
284 try:
285 tests = _GetTestsFromPickle(pickle_path, test_jar)
286 except ProguardPickleException as e:
287 logging.info('Could not get tests from pickle: %s', e)
288 logging.info('Getting tests from JAR via proguard.')
289 tests = _GetTestsFromProguard(test_jar)
290 _SaveTestsToPickle(pickle_path, test_jar, tests)
291 return tests
292
293
294 def _GetTestsFromPickle(pickle_path, jar_path):
295 if not os.path.exists(pickle_path):
296 raise ProguardPickleException('%s does not exist.' % pickle_path)
297 if os.path.getmtime(pickle_path) <= os.path.getmtime(jar_path):
298 raise ProguardPickleException(
299 '%s newer than %s.' % (jar_path, pickle_path))
300
301 with open(pickle_path, 'r') as pickle_file:
302 pickle_data = pickle.loads(pickle_file.read())
303 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
304
305 if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
306 raise ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
307 if pickle_data['JAR_MD5SUM'] != jar_md5:
308 raise ProguardPickleException('JAR file MD5 sum differs.')
309 return pickle_data['TEST_METHODS']
310
311
312 def _GetTestsFromProguard(jar_path):
313 p = proguard.Dump(jar_path)
314 class_lookup = dict((c['class'], c) for c in p['classes'])
315
316 def is_test_class(c):
317 return c['class'].endswith('Test')
318
319 def is_test_method(m):
320 return m['method'].startswith('test')
321
322 def recursive_class_annotations(c):
323 s = c['superclass']
324 if s in class_lookup:
325 a = recursive_class_annotations(class_lookup[s])
326 else:
327 a = {}
328 a.update(c['annotations'])
329 return a
330
331 def stripped_test_class(c):
332 return {
333 'class': c['class'],
334 'annotations': recursive_class_annotations(c),
335 'methods': [m for m in c['methods'] if is_test_method(m)],
336 }
337
338 return [stripped_test_class(c) for c in p['classes']
339 if is_test_class(c)]
340
341
342 def _SaveTestsToPickle(pickle_path, jar_path, tests):
343 jar_md5 = md5sum.CalculateHostMd5Sums(jar_path)[jar_path]
344 pickle_data = {
345 'VERSION': _PICKLE_FORMAT_VERSION,
346 'JAR_MD5SUM': jar_md5,
347 'TEST_METHODS': tests,
348 }
349 with open(pickle_path, 'w') as pickle_file:
350 pickle.dump(pickle_data, pickle_file)
351
352
201 class InstrumentationTestInstance(test_instance.TestInstance): 353 class InstrumentationTestInstance(test_instance.TestInstance):
202 354
203 def __init__(self, args, isolate_delegate, error_func): 355 def __init__(self, args, isolate_delegate, error_func):
204 super(InstrumentationTestInstance, self).__init__() 356 super(InstrumentationTestInstance, self).__init__()
205 357
206 self._additional_apks = [] 358 self._additional_apks = []
207 self._apk_under_test = None 359 self._apk_under_test = None
208 self._apk_under_test_incremental_install_script = None 360 self._apk_under_test_incremental_install_script = None
209 self._package_info = None 361 self._package_info = None
210 self._suite = None 362 self._suite = None
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
287 439
288 self._test_package = self._test_apk.GetPackageName() 440 self._test_package = self._test_apk.GetPackageName()
289 self._test_runner = self._test_apk.GetInstrumentationName() 441 self._test_runner = self._test_apk.GetInstrumentationName()
290 442
291 self._package_info = None 443 self._package_info = None
292 if self._apk_under_test: 444 if self._apk_under_test:
293 package_under_test = self._apk_under_test.GetPackageName() 445 package_under_test = self._apk_under_test.GetPackageName()
294 for package_info in constants.PACKAGE_INFO.itervalues(): 446 for package_info in constants.PACKAGE_INFO.itervalues():
295 if package_under_test == package_info.package: 447 if package_under_test == package_info.package:
296 self._package_info = package_info 448 self._package_info = package_info
449 break
297 if not self._package_info: 450 if not self._package_info:
298 logging.warning('Unable to find package info for %s', self._test_package) 451 logging.warning('Unable to find package info for %s', self._test_package)
299 452
300 for apk in args.additional_apks: 453 for apk in args.additional_apks:
301 if not os.path.exists(apk): 454 if not os.path.exists(apk):
302 error_func('Unable to find additional APK: %s' % apk) 455 error_func('Unable to find additional APK: %s' % apk)
303 self._additional_apks = ( 456 self._additional_apks = (
304 [apk_helper.ToHelper(x) for x in args.additional_apks]) 457 [apk_helper.ToHelper(x) for x in args.additional_apks])
305 458
306 def _initializeDataDependencyAttributes(self, args, isolate_delegate): 459 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(':') 639 device_rel_path, host_rel_path = t.split(':')
487 host_abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, host_rel_path) 640 host_abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, host_rel_path)
488 self._data_deps.extend( 641 self._data_deps.extend(
489 [(host_abs_path, 642 [(host_abs_path,
490 [None, 'chrome', 'test', 'data', device_rel_path])]) 643 [None, 'chrome', 'test', 'data', device_rel_path])])
491 644
492 def GetDataDependencies(self): 645 def GetDataDependencies(self):
493 return self._data_deps 646 return self._data_deps
494 647
495 def GetTests(self): 648 def GetTests(self):
496 pickle_path = '%s-proguard.pickle' % self.test_jar 649 tests = GetAllTests(self.test_jar)
497 try: 650 filtered_tests = FilterTests(
498 tests = self._GetTestsFromPickle(pickle_path, self.test_jar) 651 tests, self._test_filter, self._annotations, self._excluded_annotations)
499 except self.ProguardPickleException as e: 652 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 653
530 # pylint: disable=no-self-use 654 # 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(filter_annotations, all_annotations):
592 return any(
593 ak in all_annotations
594 and annotation_value_matches(av, all_annotations[ak])
595 for ak, av in filter_annotations.iteritems())
596
597 def annotation_value_matches(filter_av, av):
598 if filter_av is None:
599 return True
600 elif isinstance(av, dict):
601 return filter_av in av['value']
602 elif isinstance(av, list):
603 return filter_av in av
604 return filter_av == av
605
606 filtered_classes = []
607 for c in tests:
608 filtered_methods = []
609 for m in c['methods']:
610 # Gtest filtering
611 if not gtest_filter(c, m):
612 continue
613
614 all_annotations = dict(c['annotations'])
615 all_annotations.update(m['annotations'])
616
617 # Enforce that all tests declare their size.
618 if not any(a in _VALID_ANNOTATIONS for a in all_annotations):
619 raise MissingSizeAnnotationError('%s.%s' % (c['class'], m['method']))
620
621 if (not annotation_filter(all_annotations)
622 or not excluded_annotation_filter(all_annotations)):
623 continue
624
625 filtered_methods.append(m)
626
627 if filtered_methods:
628 filtered_class = dict(c)
629 filtered_class['methods'] = filtered_methods
630 filtered_classes.append(filtered_class)
631
632 return filtered_classes
633
634 def _InflateTests(self, tests): 655 def _InflateTests(self, tests):
635 inflated_tests = [] 656 inflated_tests = []
636 for c in tests: 657 for c in tests:
637 for m in c['methods']: 658 for m in c['methods']:
638 a = dict(c['annotations']) 659 a = dict(c['annotations'])
639 a.update(m['annotations']) 660 a.update(m['annotations'])
640 inflated_tests.append({ 661 inflated_tests.append({
641 'class': c['class'], 662 'class': c['class'],
642 'method': m['method'], 663 'method': m['method'],
643 'annotations': a, 664 'annotations': a,
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
680 @staticmethod 701 @staticmethod
681 def GenerateTestResults( 702 def GenerateTestResults(
682 result_code, result_bundle, statuses, start_ms, duration_ms): 703 result_code, result_bundle, statuses, start_ms, duration_ms):
683 return GenerateTestResults(result_code, result_bundle, statuses, 704 return GenerateTestResults(result_code, result_bundle, statuses,
684 start_ms, duration_ms) 705 start_ms, duration_ms)
685 706
686 #override 707 #override
687 def TearDown(self): 708 def TearDown(self):
688 if self._isolate_delegate: 709 if self._isolate_delegate:
689 self._isolate_delegate.Clear() 710 self._isolate_delegate.Clear()
690
OLDNEW
« no previous file with comments | « no previous file | build/android/pylib/instrumentation/instrumentation_test_instance_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698