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

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

Issue 415463002: [Android] Configurable instrumentation test runner + test SDK levels. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 4 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 (c) 2013 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2013 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 """Helper class for instrumenation test jar.""" 5 """Helper class for instrumenation test jar."""
6 # pylint: disable=W0702 6 # pylint: disable=W0702
7 7
8 import collections
9 import logging 8 import logging
10 import os 9 import os
11 import pickle 10 import pickle
12 import re 11 import re
13 import sys 12 import sys
13 import tempfile
14 14
15 from pylib import cmd_helper 15 from pylib import cmd_helper
16 from pylib import constants 16 from pylib import constants
17 from pylib.device import device_utils
17 18
18 sys.path.insert(0, 19 sys.path.insert(0,
19 os.path.join(constants.DIR_SOURCE_ROOT, 20 os.path.join(constants.DIR_SOURCE_ROOT,
20 'build', 'util', 'lib', 'common')) 21 'build', 'util', 'lib', 'common'))
21 22
22 import unittest_util # pylint: disable=F0401 23 import unittest_util # pylint: disable=F0401
23 24
24 # If you change the cached output of proguard, increment this number 25 # If you change the cached output of proguard, increment this number
25 PICKLE_FORMAT_VERSION = 1 26 PICKLE_FORMAT_VERSION = 2
26 27
27 28
28 class TestJar(object): 29 class TestJar(object):
29 _ANNOTATIONS = frozenset( 30 _ANNOTATIONS = frozenset(
30 ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest', 31 ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest',
31 'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest']) 32 'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest'])
32 _DEFAULT_ANNOTATION = 'SmallTest' 33 _DEFAULT_ANNOTATION = 'SmallTest'
33 _PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$') 34 _PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
35 _PROGUARD_SUPERCLASS_RE = re.compile(r'\s*? Superclass:\s*([\S]+)$')
34 _PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$') 36 _PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
35 _PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$') 37 _PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$')
36 _PROGUARD_ANNOTATION_CONST_RE = ( 38 _PROGUARD_ANNOTATION_CONST_RE = (
37 re.compile(r'\s*?- Constant element value.*$')) 39 re.compile(r'\s*?- Constant element value.*$'))
38 _PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$') 40 _PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$')
39 41
40 def __init__(self, jar_path): 42 def __init__(self, jar_path):
41 if not os.path.exists(jar_path): 43 if not os.path.exists(jar_path):
42 raise Exception('%s not found, please build it' % jar_path) 44 raise Exception('%s not found, please build it' % jar_path)
43 45
44 self._PROGUARD_PATH = os.path.join(constants.ANDROID_SDK_ROOT, 46 self._PROGUARD_PATH = os.path.join(constants.ANDROID_SDK_ROOT,
45 'tools/proguard/lib/proguard.jar') 47 'tools/proguard/lib/proguard.jar')
46 if not os.path.exists(self._PROGUARD_PATH): 48 if not os.path.exists(self._PROGUARD_PATH):
47 self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'], 49 self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'],
48 'external/proguard/lib/proguard.jar') 50 'external/proguard/lib/proguard.jar')
49 self._jar_path = jar_path 51 self._jar_path = jar_path
50 self._annotation_map = collections.defaultdict(list)
51 self._pickled_proguard_name = self._jar_path + '-proguard.pickle' 52 self._pickled_proguard_name = self._jar_path + '-proguard.pickle'
52 self._test_methods = [] 53 self._test_methods = {}
53 if not self._GetCachedProguardData(): 54 if not self._GetCachedProguardData():
54 self._GetProguardData() 55 self._GetProguardData()
55 56
56 def _GetCachedProguardData(self): 57 def _GetCachedProguardData(self):
57 if (os.path.exists(self._pickled_proguard_name) and 58 if (os.path.exists(self._pickled_proguard_name) and
58 (os.path.getmtime(self._pickled_proguard_name) > 59 (os.path.getmtime(self._pickled_proguard_name) >
59 os.path.getmtime(self._jar_path))): 60 os.path.getmtime(self._jar_path))):
60 logging.info('Loading cached proguard output from %s', 61 logging.info('Loading cached proguard output from %s',
61 self._pickled_proguard_name) 62 self._pickled_proguard_name)
62 try: 63 try:
63 with open(self._pickled_proguard_name, 'r') as r: 64 with open(self._pickled_proguard_name, 'r') as r:
64 d = pickle.loads(r.read()) 65 d = pickle.loads(r.read())
65 if d['VERSION'] == PICKLE_FORMAT_VERSION: 66 if d['VERSION'] == PICKLE_FORMAT_VERSION:
66 self._annotation_map = d['ANNOTATION_MAP']
67 self._test_methods = d['TEST_METHODS'] 67 self._test_methods = d['TEST_METHODS']
68 return True 68 return True
69 except: 69 except:
70 logging.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache') 70 logging.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache')
71 return False 71 return False
72 72
73 def _GetProguardData(self): 73 def _GetProguardData(self):
74 proguard_output = cmd_helper.GetCmdOutput(['java', '-jar', 74 logging.info('Retrieving test methods via proguard.')
75 self._PROGUARD_PATH,
76 '-injars', self._jar_path,
77 '-dontshrink',
78 '-dontoptimize',
79 '-dontobfuscate',
80 '-dontpreverify',
81 '-dump',
82 ]).split('\n')
83 clazz = None
84 method = None
85 annotation = None
86 has_value = False
87 qualified_method = None
88 for line in proguard_output:
89 m = self._PROGUARD_CLASS_RE.match(line)
90 if m:
91 clazz = m.group(1).replace('/', '.') # Change package delim.
92 annotation = None
93 continue
94 75
95 m = self._PROGUARD_METHOD_RE.match(line) 76 with tempfile.NamedTemporaryFile() as proguard_output:
96 if m: 77 cmd_helper.RunCmd(['java', '-jar',
97 method = m.group(1) 78 self._PROGUARD_PATH,
98 annotation = None 79 '-injars', self._jar_path,
99 qualified_method = clazz + '#' + method 80 '-dontshrink',
100 if method.startswith('test') and clazz.endswith('Test'): 81 '-dontoptimize',
101 self._test_methods += [qualified_method] 82 '-dontobfuscate',
102 continue 83 '-dontpreverify',
84 '-dump', proguard_output.name])
103 85
104 if not qualified_method: 86 clazzez = {}
105 # Ignore non-method annotations.
106 continue
107 87
108 m = self._PROGUARD_ANNOTATION_RE.match(line) 88 annotation = None
109 if m: 89 annotation_has_value = False
110 annotation = m.group(1).split('/')[-1] # Ignore the annotation package. 90 clazz = None
111 self._annotation_map[qualified_method].append(annotation) 91 method = None
112 has_value = False 92
113 continue 93 for line in proguard_output:
114 if annotation: 94 if len(line) == 0:
115 if not has_value: 95 annotation = None
116 m = self._PROGUARD_ANNOTATION_CONST_RE.match(line) 96 annotation_has_value = False
97 method = None
98 continue
99
100 m = self._PROGUARD_CLASS_RE.match(line)
101 if m:
102 clazz = m.group(1).replace('/', '.')
103 clazzez[clazz] = {
104 'methods': {},
105 'annotations': {}
106 }
107 annotation = None
108 annotation_has_value = False
109 method = None
110 continue
111
112 if not clazz:
113 continue
114
115 m = self._PROGUARD_SUPERCLASS_RE.match(line)
116 if m:
117 clazzez[clazz]['superclass'] = m.group(1).replace('/', '.')
118 continue
119
120 if clazz.endswith('Test'):
121 m = self._PROGUARD_METHOD_RE.match(line)
117 if m: 122 if m:
118 has_value = True 123 method = m.group(1)
119 else: 124 clazzez[clazz]['methods'][method] = {'annotations': {}}
120 m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line) 125 annotation = None
121 if m: 126 annotation_has_value = False
122 value = m.group(1) 127 continue
123 self._annotation_map[qualified_method].append( 128
124 annotation + ':' + value) 129 m = self._PROGUARD_ANNOTATION_RE.match(line)
125 has_value = False 130 if m:
131 # Ignore the annotation package.
132 annotation = m.group(1).split('/')[-1]
133 if method:
134 clazzez[clazz]['methods'][method]['annotations'][annotation] = None
135 else:
136 clazzez[clazz]['annotations'][annotation] = None
137 continue
138
139 if annotation:
140 if not annotation_has_value:
141 m = self._PROGUARD_ANNOTATION_CONST_RE.match(line)
142 annotation_has_value = bool(m)
143 else:
144 m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line)
145 if m:
146 if method:
147 clazzez[clazz]['methods'][method]['annotations'][annotation] = (
148 m.group(1))
149 else:
150 clazzez[clazz]['annotations'][annotation] = m.group(1)
151 annotation_has_value = None
152
153 test_clazzez = ((n, i) for n, i in clazzez.items() if n.endswith('Test'))
154 for clazz_name, clazz_info in test_clazzez:
155 logging.info('Processing %s' % clazz_name)
156 c = clazz_name
157 min_sdk_level = None
158
159 while c in clazzez:
160 c_info = clazzez[c]
161 if not min_sdk_level:
162 min_sdk_level = c_info['annotations'].get('MinAndroidSdkLevel', None)
163 c = c_info.get('superclass', None)
164
165 for method_name, method_info in clazz_info['methods'].items():
166 if method_name.startswith('test'):
167 qualified_method = '%s#%s' % (clazz_name, method_name)
168 method_annotations = []
169 for annotation_name, annotation_value in (
170 method_info['annotations'].items()):
171 method_annotations.append(annotation_name)
172 if annotation_value:
173 method_annotations.append(
174 annotation_name + ':' + annotation_value)
175 self._test_methods[qualified_method] = {
176 'annotations': method_annotations
177 }
178 if min_sdk_level is not None:
179 self._test_methods[qualified_method]['min_sdk_level'] = (
180 int(min_sdk_level))
126 181
127 logging.info('Storing proguard output to %s', self._pickled_proguard_name) 182 logging.info('Storing proguard output to %s', self._pickled_proguard_name)
128 d = {'VERSION': PICKLE_FORMAT_VERSION, 183 d = {'VERSION': PICKLE_FORMAT_VERSION,
129 'ANNOTATION_MAP': self._annotation_map,
130 'TEST_METHODS': self._test_methods} 184 'TEST_METHODS': self._test_methods}
131 with open(self._pickled_proguard_name, 'w') as f: 185 with open(self._pickled_proguard_name, 'w') as f:
132 f.write(pickle.dumps(d)) 186 f.write(pickle.dumps(d))
133 187
134 def _GetAnnotationMap(self):
135 return self._annotation_map
136 188
137 @staticmethod 189 @staticmethod
138 def _IsTestMethod(test): 190 def _IsTestMethod(test):
139 class_name, method = test.split('#') 191 class_name, method = test.split('#')
140 return class_name.endswith('Test') and method.startswith('test') 192 return class_name.endswith('Test') and method.startswith('test')
141 193
142 def GetTestAnnotations(self, test): 194 def GetTestAnnotations(self, test):
143 """Returns a list of all annotations for the given |test|. May be empty.""" 195 """Returns a list of all annotations for the given |test|. May be empty."""
144 if not self._IsTestMethod(test): 196 if not self._IsTestMethod(test) or not test in self._test_methods:
145 return [] 197 return []
146 return self._GetAnnotationMap()[test] 198 return self._test_methods[test]['annotations']
147 199
148 @staticmethod 200 @staticmethod
149 def _AnnotationsMatchFilters(annotation_filter_list, annotations): 201 def _AnnotationsMatchFilters(annotation_filter_list, annotations):
150 """Checks if annotations match any of the filters.""" 202 """Checks if annotations match any of the filters."""
151 if not annotation_filter_list: 203 if not annotation_filter_list:
152 return True 204 return True
153 for annotation_filter in annotation_filter_list: 205 for annotation_filter in annotation_filter_list:
154 filters = annotation_filter.split('=') 206 filters = annotation_filter.split('=')
155 if len(filters) == 2: 207 if len(filters) == 2:
156 key = filters[0] 208 key = filters[0]
157 value_list = filters[1].split(',') 209 value_list = filters[1].split(',')
158 for value in value_list: 210 for value in value_list:
159 if key + ':' + value in annotations: 211 if key + ':' + value in annotations:
160 return True 212 return True
161 elif annotation_filter in annotations: 213 elif annotation_filter in annotations:
162 return True 214 return True
163 return False 215 return False
164 216
165 def GetAnnotatedTests(self, annotation_filter_list): 217 def GetAnnotatedTests(self, annotation_filter_list):
166 """Returns a list of all tests that match the given annotation filters.""" 218 """Returns a list of all tests that match the given annotation filters."""
167 return [test for test, annotations in self._GetAnnotationMap().iteritems() 219 return [test for test, attr in self.GetTestMethods().iteritems()
168 if self._IsTestMethod(test) and self._AnnotationsMatchFilters( 220 if self._IsTestMethod(test) and self._AnnotationsMatchFilters(
169 annotation_filter_list, annotations)] 221 annotation_filter_list, attr['annotations'])]
170 222
171 def GetTestMethods(self): 223 def GetTestMethods(self):
172 """Returns a list of all test methods in this apk as Class#testMethod.""" 224 """Returns a dict of all test methods and relevant attributes.
225
226 Test methods are retrieved as Class#testMethod.
227 """
173 return self._test_methods 228 return self._test_methods
174 229
175 def _GetTestsMissingAnnotation(self): 230 def _GetTestsMissingAnnotation(self):
176 """Get a list of test methods with no known annotations.""" 231 """Get a list of test methods with no known annotations."""
177 tests_missing_annotations = [] 232 tests_missing_annotations = []
178 for test_method in self.GetTestMethods(): 233 for test_method in self.GetTestMethods().iterkeys():
179 annotations_ = frozenset(self.GetTestAnnotations(test_method)) 234 annotations_ = frozenset(self.GetTestAnnotations(test_method))
180 if (annotations_.isdisjoint(self._ANNOTATIONS) and 235 if (annotations_.isdisjoint(self._ANNOTATIONS) and
181 not self.IsHostDrivenTest(test_method)): 236 not self.IsHostDrivenTest(test_method)):
182 tests_missing_annotations.append(test_method) 237 tests_missing_annotations.append(test_method)
183 return sorted(tests_missing_annotations) 238 return sorted(tests_missing_annotations)
184 239
240 def _IsTestValidForSdkRange(self, test_name, attached_min_sdk_level):
241 required_min_sdk_level = self.GetTestMethods()[test_name].get(
242 'min_sdk_level', None)
243 return (required_min_sdk_level is None or
244 attached_min_sdk_level >= required_min_sdk_level)
245
185 def GetAllMatchingTests(self, annotation_filter_list, 246 def GetAllMatchingTests(self, annotation_filter_list,
186 exclude_annotation_list, test_filter): 247 exclude_annotation_list, test_filter):
187 """Get a list of tests matching any of the annotations and the filter. 248 """Get a list of tests matching any of the annotations and the filter.
188 249
189 Args: 250 Args:
190 annotation_filter_list: List of test annotations. A test must have at 251 annotation_filter_list: List of test annotations. A test must have at
191 least one of these annotations. A test without any annotations is 252 least one of these annotations. A test without any annotations is
192 considered to be SmallTest. 253 considered to be SmallTest.
193 exclude_annotation_list: List of test annotations. A test must not have 254 exclude_annotation_list: List of test annotations. A test must not have
194 any of these annotations. 255 any of these annotations.
(...skipping 26 matching lines...) Expand all
221 sanitized_test_names = dict([ 282 sanitized_test_names = dict([
222 (t.split('.')[-1].replace('#', '.'), t) for t in available_tests]) 283 (t.split('.')[-1].replace('#', '.'), t) for t in available_tests])
223 # Filters 'class.test' names and populates |tests| with the corresponding 284 # Filters 'class.test' names and populates |tests| with the corresponding
224 # 'package.path.class#test' names. 285 # 'package.path.class#test' names.
225 tests = [ 286 tests = [
226 sanitized_test_names[t] for t in unittest_util.FilterTestNames( 287 sanitized_test_names[t] for t in unittest_util.FilterTestNames(
227 sanitized_test_names.keys(), test_filter.replace('#', '.'))] 288 sanitized_test_names.keys(), test_filter.replace('#', '.'))]
228 else: 289 else:
229 tests = available_tests 290 tests = available_tests
230 291
292 # Filter out any tests with SDK level requirements that don't match the set
293 # of attached devices.
294 sdk_versions = [
295 int(v) for v in
296 device_utils.DeviceUtils.parallel().GetProp(
297 'ro.build.version.sdk').pGet(None)]
298 tests = filter(
299 lambda t: self._IsTestValidForSdkRange(t, min(sdk_versions)),
300 tests)
301
231 return tests 302 return tests
232 303
233 @staticmethod 304 @staticmethod
234 def IsHostDrivenTest(test): 305 def IsHostDrivenTest(test):
235 return 'pythonDrivenTests' in test 306 return 'pythonDrivenTests' in test
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698