OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 | |
OLD | NEW |