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

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

Issue 2491373005: [Android] Fix instrumentation test filtering for parameterized tests. (Closed)
Patch Set: Created 4 years, 1 month 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/local/device/local_device_instrumentation_test_run.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 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
43 _EXTRA_DRIVER_TARGET_PACKAGE = ( 43 _EXTRA_DRIVER_TARGET_PACKAGE = (
44 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage') 44 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage')
45 _EXTRA_DRIVER_TARGET_CLASS = ( 45 _EXTRA_DRIVER_TARGET_CLASS = (
46 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass') 46 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass')
47 _EXTRA_TIMEOUT_SCALE = ( 47 _EXTRA_TIMEOUT_SCALE = (
48 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TimeoutScale') 48 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TimeoutScale')
49 49
50 _PARAMETERIZED_TEST_ANNOTATION = 'ParameterizedTest' 50 _PARAMETERIZED_TEST_ANNOTATION = 'ParameterizedTest'
51 _PARAMETERIZED_TEST_SET_ANNOTATION = 'ParameterizedTest$Set' 51 _PARAMETERIZED_TEST_SET_ANNOTATION = 'ParameterizedTest$Set'
52 _NATIVE_CRASH_RE = re.compile('(process|native) crash', re.IGNORECASE) 52 _NATIVE_CRASH_RE = re.compile('(process|native) crash', re.IGNORECASE)
53 _CMDLINE_NAME_SEGMENT_RE = re.compile(
54 r' with(?:out)? \{[^\}]*\}')
mikecase (-- gone --) 2016/11/12 01:55:08 so, I could easily be wrong, but with this work on
jbudorick 2016/11/14 16:26:32 Yes, it should: https://docs.python.org/2/library/
53 _PICKLE_FORMAT_VERSION = 10 55 _PICKLE_FORMAT_VERSION = 10
54 56
55 57
56 class MissingSizeAnnotationError(test_exception.TestException): 58 class MissingSizeAnnotationError(test_exception.TestException):
57 def __init__(self, class_name): 59 def __init__(self, class_name):
58 super(MissingSizeAnnotationError, self).__init__(class_name + 60 super(MissingSizeAnnotationError, self).__init__(class_name +
59 ': Test method is missing required size annotation. Add one of: ' + 61 ': Test method is missing required size annotation. Add one of: ' +
60 ', '.join('@' + a for a in _VALID_ANNOTATIONS)) 62 ', '.join('@' + a for a in _VALID_ANNOTATIONS))
61 63
62 64
(...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after
213 tests: a list of tests. e.g. [ 215 tests: a list of tests. e.g. [
214 {'annotations": {}, 'class': 'com.example.TestA', 'methods':[]}, 216 {'annotations": {}, 'class': 'com.example.TestA', 'methods':[]},
215 {'annotations": {}, 'class': 'com.example.TestB', 'methods':[]}] 217 {'annotations": {}, 'class': 'com.example.TestB', 'methods':[]}]
216 test_filter: googletest-style filter string. 218 test_filter: googletest-style filter string.
217 annotations: a dict of wanted annotations for test methods. 219 annotations: a dict of wanted annotations for test methods.
218 exclude_annotations: a dict of annotations to exclude. 220 exclude_annotations: a dict of annotations to exclude.
219 221
220 Return: 222 Return:
221 A list of filtered tests 223 A list of filtered tests
222 """ 224 """
223 def gtest_filter(c, m): 225 def gtest_filter(t):
224 if not test_filter: 226 if not test_filter:
225 return True 227 return True
226 # Allow fully-qualified name as well as an omitted package. 228 # Allow fully-qualified name as well as an omitted package.
227 names = ['%s.%s' % (c['class'], m['method']), 229 unqualified_class_test = {
mikecase (-- gone --) 2016/11/12 01:55:09 unqualified_class_test is just like package-name-l
jbudorick 2016/11/14 16:26:32 Acknowledged.
228 '%s.%s' % (c['class'].split('.')[-1], m['method'])] 230 'class': t['class'].split('.')[-1],
231 'method': t['method']
232 }
233 names = [
234 GetTestName(t, sep='.'),
235 GetTestName(unqualified_class_test, sep='.'),
236 GetUniqueTestName(t, sep='.')
237 ]
mikecase (-- gone --) 2016/11/12 01:55:09 I like this solution. :D
jbudorick 2016/11/14 16:26:32 Yeah, it's a bit more readable this way.
229 return unittest_util.FilterTestNames(names, test_filter) 238 return unittest_util.FilterTestNames(names, test_filter)
230 239
231 def annotation_filter(all_annotations): 240 def annotation_filter(all_annotations):
232 if not annotations: 241 if not annotations:
233 return True 242 return True
234 return any_annotation_matches(annotations, all_annotations) 243 return any_annotation_matches(annotations, all_annotations)
235 244
236 def excluded_annotation_filter(all_annotations): 245 def excluded_annotation_filter(all_annotations):
237 if not excluded_annotations: 246 if not excluded_annotations:
238 return True 247 return True
239 return not any_annotation_matches(excluded_annotations, 248 return not any_annotation_matches(excluded_annotations,
240 all_annotations) 249 all_annotations)
241 250
242 def any_annotation_matches(filter_annotations, all_annotations): 251 def any_annotation_matches(filter_annotations, all_annotations):
243 return any( 252 return any(
244 ak in all_annotations 253 ak in all_annotations
245 and annotation_value_matches(av, all_annotations[ak]) 254 and annotation_value_matches(av, all_annotations[ak])
246 for ak, av in filter_annotations) 255 for ak, av in filter_annotations)
247 256
248 def annotation_value_matches(filter_av, av): 257 def annotation_value_matches(filter_av, av):
249 if filter_av is None: 258 if filter_av is None:
250 return True 259 return True
251 elif isinstance(av, dict): 260 elif isinstance(av, dict):
252 return filter_av in av['value'] 261 return filter_av in av['value']
253 elif isinstance(av, list): 262 elif isinstance(av, list):
254 return filter_av in av 263 return filter_av in av
255 return filter_av == av 264 return filter_av == av
256 265
257 filtered_classes = [] 266 filtered_tests = []
258 for c in tests: 267 for t in tests:
259 filtered_methods = [] 268 # Gtest filtering
260 for m in c['methods']: 269 if not gtest_filter(t):
261 # Gtest filtering 270 continue
262 if not gtest_filter(c, m):
263 continue
264 271
265 all_annotations = dict(c['annotations']) 272 # Enforce that all tests declare their size.
266 all_annotations.update(m['annotations']) 273 if not any(a in _VALID_ANNOTATIONS for a in t['annotations']):
274 raise MissingSizeAnnotationError(GetTestName(t))
267 275
268 # Enforce that all tests declare their size. 276 if (not annotation_filter(t['annotations'])
269 if not any(a in _VALID_ANNOTATIONS for a in all_annotations): 277 or not excluded_annotation_filter(t['annotations'])):
270 raise MissingSizeAnnotationError('%s.%s' % (c['class'], m['method'])) 278 continue
271 279
272 if (not annotation_filter(all_annotations) 280 filtered_tests.append(t)
273 or not excluded_annotation_filter(all_annotations)):
274 continue
275 281
276 filtered_methods.append(m) 282 return filtered_tests
277 283
278 if filtered_methods:
279 filtered_class = dict(c)
280 filtered_class['methods'] = filtered_methods
281 filtered_classes.append(filtered_class)
282
283 return filtered_classes
284 284
285 def GetAllTests(test_jar): 285 def GetAllTests(test_jar):
286 pickle_path = '%s-proguard.pickle' % test_jar 286 pickle_path = '%s-proguard.pickle' % test_jar
287 try: 287 try:
288 tests = _GetTestsFromPickle(pickle_path, test_jar) 288 tests = _GetTestsFromPickle(pickle_path, test_jar)
289 except ProguardPickleException as e: 289 except ProguardPickleException as e:
290 logging.info('Could not get tests from pickle: %s', e) 290 logging.info('Could not get tests from pickle: %s', e)
291 logging.info('Getting tests from JAR via proguard.') 291 logging.info('Getting tests from JAR via proguard.')
292 tests = _GetTestsFromProguard(test_jar) 292 tests = _GetTestsFromProguard(test_jar)
293 _SaveTestsToPickle(pickle_path, test_jar, tests) 293 _SaveTestsToPickle(pickle_path, test_jar, tests)
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after
354 354
355 355
356 class UnmatchedFilterException(test_exception.TestException): 356 class UnmatchedFilterException(test_exception.TestException):
357 """Raised when a user specifies a filter that doesn't match any tests.""" 357 """Raised when a user specifies a filter that doesn't match any tests."""
358 358
359 def __init__(self, test_filter): 359 def __init__(self, test_filter):
360 super(UnmatchedFilterException, self).__init__( 360 super(UnmatchedFilterException, self).__init__(
361 'Test filter "%s" matched no tests.' % test_filter) 361 'Test filter "%s" matched no tests.' % test_filter)
362 362
363 363
364 def GetTestName(test, sep='#'):
365 """Gets the name of the given test.
366
367 Note that this may return the same name for more than one test, e.g. if a
368 test is being run multiple times with different parameters.
369
370 Args:
371 test: the instrumentation test dict.
372 sep: the character(s) that should join the class name and the method name.
373 Returns:
374 The test name as a string.
375 """
376 return '%s%s%s' % (test['class'], sep, test['method'])
377
378
379 def GetUniqueTestName(test, sep='#'):
380 """Gets the unique name of the given test.
381
382 This will include text to disambiguate between tests for which GetTestName
383 would return the same name.
384
385 Args:
386 test: the instrumentation test dict.
387 sep: the character(s) that should join the class name and the method name.
388 Returns:
389 The unique test name as a string.
390 """
391 display_name = GetTestName(test, sep=sep)
392 if 'flags' in test:
393 flags = test['flags']
394 if flags.add:
395 display_name = '%s with {%s}' % (display_name, ' '.join(flags.add))
396 if flags.remove:
397 display_name = '%s without {%s}' % (display_name, ' '.join(flags.remove))
398 return display_name
399
400
364 class InstrumentationTestInstance(test_instance.TestInstance): 401 class InstrumentationTestInstance(test_instance.TestInstance):
365 402
366 def __init__(self, args, isolate_delegate, error_func): 403 def __init__(self, args, isolate_delegate, error_func):
367 super(InstrumentationTestInstance, self).__init__() 404 super(InstrumentationTestInstance, self).__init__()
368 405
369 self._additional_apks = [] 406 self._additional_apks = []
370 self._apk_under_test = None 407 self._apk_under_test = None
371 self._apk_under_test_incremental_install_script = None 408 self._apk_under_test_incremental_install_script = None
372 self._package_info = None 409 self._package_info = None
373 self._suite = None 410 self._suite = None
(...skipping 122 matching lines...) Expand 10 before | Expand all | Expand 10 after
496 self._isolated_abs_path = os.path.join( 533 self._isolated_abs_path = os.path.join(
497 constants.GetOutDirectory(), '%s.isolated' % self._test_package) 534 constants.GetOutDirectory(), '%s.isolated' % self._test_package)
498 else: 535 else:
499 self._isolate_delegate = None 536 self._isolate_delegate = None
500 537
501 if not self._isolate_delegate: 538 if not self._isolate_delegate:
502 logging.warning('No data dependencies will be pushed.') 539 logging.warning('No data dependencies will be pushed.')
503 540
504 def _initializeTestFilterAttributes(self, args): 541 def _initializeTestFilterAttributes(self, args):
505 if args.test_filter: 542 if args.test_filter:
506 self._test_filter = args.test_filter.replace('#', '.') 543 self._test_filter = _CMDLINE_NAME_SEGMENT_RE.sub(
544 '', args.test_filter.replace('#', '.'))
507 545
508 def annotation_element(a): 546 def annotation_element(a):
509 a = a.split('=', 1) 547 a = a.split('=', 1)
510 return (a[0], a[1] if len(a) == 2 else None) 548 return (a[0], a[1] if len(a) == 2 else None)
511 549
512 if args.annotation_str: 550 if args.annotation_str:
513 self._annotations = [ 551 self._annotations = [
514 annotation_element(a) for a in args.annotation_str.split(',')] 552 annotation_element(a) for a in args.annotation_str.split(',')]
515 elif not self._test_filter: 553 elif not self._test_filter:
516 self._annotations = [ 554 self._annotations = [
(...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after
654 self._isolate_delegate.Remap( 692 self._isolate_delegate.Remap(
655 self._isolate_abs_path, self._isolated_abs_path) 693 self._isolate_abs_path, self._isolated_abs_path)
656 self._isolate_delegate.MoveOutputDeps() 694 self._isolate_delegate.MoveOutputDeps()
657 self._data_deps.extend([(self._isolate_delegate.isolate_deps_dir, None)]) 695 self._data_deps.extend([(self._isolate_delegate.isolate_deps_dir, None)])
658 696
659 def GetDataDependencies(self): 697 def GetDataDependencies(self):
660 return self._data_deps 698 return self._data_deps
661 699
662 def GetTests(self): 700 def GetTests(self):
663 tests = GetAllTests(self.test_jar) 701 tests = GetAllTests(self.test_jar)
702 inflated_tests = self._ParametrizeTestsWithFlags(self._InflateTests(tests))
664 filtered_tests = FilterTests( 703 filtered_tests = FilterTests(
665 tests, self._test_filter, self._annotations, self._excluded_annotations) 704 inflated_tests, self._test_filter, self._annotations,
705 self._excluded_annotations)
666 if self._test_filter and not filtered_tests: 706 if self._test_filter and not filtered_tests:
707 for t in inflated_tests:
708 logging.debug(' %s', GetUniqueTestName(t))
667 raise UnmatchedFilterException(self._test_filter) 709 raise UnmatchedFilterException(self._test_filter)
668 return self._ParametrizeTestsWithFlags(self._InflateTests(filtered_tests)) 710 return filtered_tests
669 711
670 # pylint: disable=no-self-use 712 # pylint: disable=no-self-use
671 def _InflateTests(self, tests): 713 def _InflateTests(self, tests):
672 inflated_tests = [] 714 inflated_tests = []
673 for c in tests: 715 for c in tests:
674 for m in c['methods']: 716 for m in c['methods']:
675 a = dict(c['annotations']) 717 a = dict(c['annotations'])
676 a.update(m['annotations']) 718 a.update(m['annotations'])
677 inflated_tests.append({ 719 inflated_tests.append({
678 'class': c['class'], 720 'class': c['class'],
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
717 @staticmethod 759 @staticmethod
718 def GenerateTestResults( 760 def GenerateTestResults(
719 result_code, result_bundle, statuses, start_ms, duration_ms): 761 result_code, result_bundle, statuses, start_ms, duration_ms):
720 return GenerateTestResults(result_code, result_bundle, statuses, 762 return GenerateTestResults(result_code, result_bundle, statuses,
721 start_ms, duration_ms) 763 start_ms, duration_ms)
722 764
723 #override 765 #override
724 def TearDown(self): 766 def TearDown(self):
725 if self._isolate_delegate: 767 if self._isolate_delegate:
726 self._isolate_delegate.Clear() 768 self._isolate_delegate.Clear()
OLDNEW
« no previous file with comments | « no previous file | build/android/pylib/local/device/local_device_instrumentation_test_run.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698