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