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

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

Issue 794923003: [Android] Implement instrumentation tests in platform mode. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 11 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
(Empty)
1
klundberg 2015/01/07 22:56:30 Copyright?
jbudorick 2015/01/12 16:32:16 oops. done
2 import logging
3 import os
4 import pickle
5 import sys
6
7 from pylib import cmd_helper
8 from pylib import constants
9 from pylib import flag_changer
10 from pylib.base import base_test_result
11 from pylib.base import test_instance
12 from pylib.instrumentation import test_result
13 from pylib.utils import apk_helper
14 from pylib.utils import md5sum
15 from pylib.utils import proguard
16
17 sys.path.append(
18 os.path.join(constants.DIR_SOURCE_ROOT, 'build', 'util', 'lib', 'common'))
19 import unittest_util
20
21 _DEFAULT_ANNOTATIONS = [
22 'Smoke', 'SmallTest', 'MediumTest', 'LargeTest',
23 'EnormousTest', 'IntegrationTest']
24 _PICKLE_FORMAT_VERSION = 10
25
26
27 # TODO(jbudorick): Make these private class methods of
28 # InstrumentationTestInstance once the instrumentation test_runner is
29 # deprecated.
30 def ParseAmInstrumentRawOutput(raw_output):
31 """Parses the output of an |am instrument -r| call.
32
33 Args:
34 raw_output: the output of an |am instrument -r| call as a list of lines
35 Returns:
36 A 3-tuple containing:
37 - the instrumentation code as an integer
38 - the instrumentation result as a list of lines
39 - the instrumentation statuses received as a list of 2-tuples
40 containing:
41 - the status code as an integer
42 - the bundle dump as a dict mapping string keys to a list of
43 strings, one for each line.
44 """
45 INSTR_STATUS = 'INSTRUMENTATION_STATUS: '
46 INSTR_STATUS_CODE = 'INSTRUMENTATION_STATUS_CODE: '
47 INSTR_RESULT = 'INSTRUMENTATION_RESULT: '
48 INSTR_CODE = 'INSTRUMENTATION_CODE: '
49
50 last = None
51 instr_code = None
52 instr_result = []
53 instr_statuses = []
54 bundle = {}
55 for line in raw_output:
56 if line.startswith(INSTR_STATUS):
57 instr_var = line[len(INSTR_STATUS):]
58 if '=' in instr_var:
59 k, v = instr_var.split('=', 1)
60 bundle[k] = [v]
61 last = INSTR_STATUS
62 last_key = k
63 else:
64 logging.debug('Unknown "%s" line: %s' % (INSTR_STATUS, line))
65
66 elif line.startswith(INSTR_STATUS_CODE):
67 instr_status = line[len(INSTR_STATUS_CODE):]
68 instr_statuses.append((int(instr_status), bundle))
69 bundle = {}
70 last = INSTR_STATUS_CODE
71
72 elif line.startswith(INSTR_RESULT):
73 instr_result.append(line[len(INSTR_RESULT):])
74 last = INSTR_RESULT
75
76 elif line.startswith(INSTR_CODE):
77 instr_code = int(line[len(INSTR_CODE):])
78 last = INSTR_CODE
79
80 elif last == INSTR_STATUS:
81 bundle[last_key].append(line)
82
83 elif last == INSTR_RESULT:
84 instr_result.append(line)
85
86 return (instr_code, instr_result, instr_statuses)
87
88
89 def GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms):
90 """Generate the result of |test| from |instr_statuses|.
91
92 Args:
93 test_name: The name of the test as "class#method"
94 instr_statuses: A list of 2-tuples containing:
95 - the status code as an integer
96 - the bundle dump as a dict mapping string keys to string values
97 Note that this is the same as the third item in the 3-tuple returned by
98 |_ParseAmInstrumentRawOutput|.
99 start_ms: The start time of the test in milliseconds.
100 duration_ms: The duration of the test in milliseconds.
101 Returns:
102 An InstrumentationTestResult object.
103 """
104 INSTR_STATUS_CODE_START = 1
105 INSTR_STATUS_CODE_OK = 0
106 INSTR_STATUS_CODE_ERROR = -1
107 INSTR_STATUS_CODE_FAIL = -2
108
109 log = ''
110 result_type = base_test_result.ResultType.UNKNOWN
111
112 for status_code, bundle in instr_statuses:
113 if status_code == INSTR_STATUS_CODE_START:
114 pass
115 elif status_code == INSTR_STATUS_CODE_OK:
116 bundle_test = '%s#%s' % (
117 ''.join(bundle.get('class', [''])),
118 ''.join(bundle.get('test', [''])))
119 skipped = ''.join(bundle.get('test_skipped', ['']))
120
121 if (test_name == bundle_test and
122 result_type == base_test_result.ResultType.UNKNOWN):
123 result_type = base_test_result.ResultType.PASS
124 elif skipped.lower() in ('true', '1', 'yes'):
125 result_type = base_test_result.ResultType.SKIP
126 logging.info('Skipped ' + test_name)
127 else:
128 if status_code not in (INSTR_STATUS_CODE_ERROR,
129 INSTR_STATUS_CODE_FAIL):
130 logging.info('Unrecognized status code %d. Handling as an error.',
131 status_code)
klundberg 2015/01/07 22:56:30 Maybe log this as an error instead? Seems like a
jbudorick 2015/01/12 16:32:16 Done. I do think an exception would be a bit much
132 result_type = base_test_result.ResultType.FAIL
133 if 'stack' in bundle:
134 log = '\n'.join(bundle['stack'])
135
136 print 'test: %r' % test_name
klundberg 2015/01/07 22:56:30 Why print and not logging.info? Is this just lefto
jbudorick 2015/01/12 16:32:16 Yep. Removed.
137 return test_result.InstrumentationTestResult(
138 test_name, result_type, start_ms, duration_ms, log=log)
139
140
141 class InstrumentationTestInstance(test_instance.TestInstance):
142
143 def __init__(self, args, isolate_delegate, error_func):
144 super(InstrumentationTestInstance, self).__init__()
145
146 # APKs
klundberg 2015/01/07 22:56:30 This comment isn't very descriptive. Maybe remove
jbudorick 2015/01/12 16:32:16 This comment (along with "Data dependencies", "Tes
klundberg 2015/01/12 17:16:17 I think this is much better. Much easier to read.
147 if args.apk_under_test.endswith('.apk'):
148 self._apk_under_test = args.apk_under_test
klundberg 2015/01/07 22:56:30 For the test apk, you check whether the apk exists
jbudorick 2015/01/12 16:32:16 I made the two consistent by removing what I think
149 else:
150 self._apk_under_test = os.path.join(
151 constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
152 '%s.apk' % args.apk_under_test)
153
154 if not os.path.exists(self._apk_under_test):
155 error_func('Unable to find APK under test: %s' % self._apk_under_test)
156
157 if args.test_apk.endswith('.apk'):
158 test_apk_root = os.path.splitext(os.path.basename(args.test_apk))[0]
159 if os.path.exists(args.test_apk):
160 self._test_apk = args.test_apk
161 else:
162 self._test_apk = os.path.join(
163 constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
164 args.test_apk)
165 else:
166 test_apk_root = args.test_apk
167 self._test_apk = os.path.join(
168 constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
169 '%s.apk' % args.test_apk)
klundberg 2015/01/07 22:56:30 nit: Would be nice with an empty line here.
jbudorick 2015/01/12 16:32:16 Done.
170 self._test_jar = os.path.join(
171 constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
172 '%s.jar' % test_apk_root)
173 self._test_support_apk = os.path.join(
174 constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
175 '%sSupport.apk' % test_apk_root)
176
177 if not os.path.exists(self._test_apk):
178 error_func('Unable to find test APK: %s' % self._test_apk)
179 if not os.path.exists(self._test_jar):
180 error_func('Unable to find test JAR: %s' % self._test_jar)
181
182 self._test_package = apk_helper.GetPackageName(self.test_apk)
183 self._test_runner = apk_helper.GetInstrumentationName(self.test_apk)
184
185 self._package_info = None
186 for package_info in constants.PACKAGE_INFO.itervalues():
187 if self._test_package == package_info.test_package:
188 self._package_info = package_info
189 if not self._package_info:
190 error_func('Unable to find package info for %s' % self._test_package)
191
192 # Data dependencies
193 self._data_deps = []
194 if args.isolate_file_path:
195 self._isolate_abs_path = os.path.abspath(args.isolate_file_path)
196 self._isolate_delegate = isolate_delegate
197 self._isolated_abs_path = os.path.join(
198 constants.GetOutDirectory(), '%s.isolated' % self._test_package)
199 else:
200 self._isolate_delegate = None
201
202 if args.test_data:
klundberg 2015/01/07 22:56:30 Should we have a TODO to deprecate this? I noticed
jbudorick 2015/01/12 16:32:16 Done.
203 logging.info('Data dependencies specified via --test-data')
204 self._test_data = args.test_data
205 else:
206 self._test_data = None
207
208 if not self._isolate_delegate and not self._test_data:
209 logging.warning('No data dependencies will be pushed.')
210
211 # Test filters
212 self._test_filter = args.test_filter
213
214 def annotation_dict_element(a):
215 a = a.split('=')
216 return (a[0], a[1] if len(a) == 2 else None)
217
218 if args.annotation_str:
219 self._annotations = dict(
220 annotation_dict_element(a)
221 for a in args.annotation_str.split(','))
222 elif not self._test_filter:
223 self._annotations = dict(
224 annotation_dict_element(a)
225 for a in _DEFAULT_ANNOTATIONS)
226 else:
227 self._annotations = {}
228
229 if args.exclude_annotation_str:
230 self._excluded_annotations = dict(
231 annotation_dict_element(a)
232 for a in args.exclude_annotation_str.split(','))
233 else:
234 self._excluded_annotations = {}
235
236 # Flags
237 self._flags = ['--disable-fre', '--enable-test-intents']
238 # TODO(jbudorick): Transition "--device-flags" to "--device-flags-file"
239 if hasattr(args, 'device_flags') and args.device_flags:
240 with open(args.device_flags) as device_flags_file:
241 stripped_lines = (l.strip() for l in device_flags_file)
242 self.flags.extend([flag for flag in stripped_lines if flag])
243 if hasattr(args, 'device_flags_file') and args.device_flags_file:
244 with open(args.device_flags_file) as device_flags_file:
245 stripped_lines = (l.strip() for l in device_flags_file)
246 self.flags.extend([flag for flag in stripped_lines if flag])
247
248 @property
249 def apk_under_test(self):
250 return self._apk_under_test
251
252 @property
253 def flags(self):
254 return self._flags
255
256 @property
257 def package_info(self):
258 return self._package_info
259
260 @property
261 def test_apk(self):
262 return self._test_apk
263
264 @property
265 def test_jar(self):
266 return self._test_jar
267
268 @property
269 def test_support_apk(self):
270 return self._test_support_apk
271
272 @property
273 def test_package(self):
274 return self._test_package
275
276 @property
277 def test_runner(self):
278 return self._test_runner
279
280 #override
281 def TestType(self):
282 return 'instrumentation'
283
284 #override
285 def SetUp(self):
286 if self._isolate_delegate:
287 self._isolate_delegate.Remap(
288 self._isolate_abs_path, self._isolated_abs_path)
289 self._isolate_delegate.MoveOutputDeps()
290 self._data_deps.extend([(constants.ISOLATE_DEPS_DIR, None)])
291
292 # TODO(jbudorick): Convert existing tests that depend on the --test-data
293 # mechanism to isolate, then remove this.
294 if self._test_data:
295 for t in self._test_data:
296 device_rel_path, host_rel_path = t.split(':')
297 host_abs_path = os.path.join(constants.DIR_SOURCE_ROOT, host_rel_path)
298 self._data_deps.extend(
299 [(host_abs_path,
300 [None, 'chrome', 'test', 'data', device_rel_path])])
301
302 def GetDataDependencies(self):
303 return self._data_deps
304
305 def GetTests(self):
306 pickle_path = '%s-proguard.pickle' % self.test_jar
307 try:
308 tests = self._GetTestsFromPickle(pickle_path, self.test_jar)
309 except self.ProguardPickleException as e:
310 logging.info('Getting tests from JAR via proguard. (%s)' % str(e))
311 tests = self._GetTestsFromProguard(self.test_jar)
312 self._SaveTestsToPickle(pickle_path, self.test_jar, tests)
313 return self._InflateTests(self._FilterTests(tests))
314
315 class ProguardPickleException(Exception):
316 pass
317
318 def _GetTestsFromPickle(self, pickle_path, jar_path):
319 if not os.path.exists(pickle_path):
320 raise self.ProguardPickleException('%s does not exist.' % pickle_path)
321 if os.path.getmtime(pickle_path) <= os.path.getmtime(jar_path):
322 raise self.ProguardPickleException(
323 '%s newer than %s.' % (jar_path, pickle_path))
324
325 with open(pickle_path, 'r') as pickle_file:
326 pickle_data = pickle.loads(pickle_file.read())
327 jar_md5, _ = md5sum.CalculateHostMd5Sums(jar_path)[0]
328
329 try:
330 if pickle_data['VERSION'] != _PICKLE_FORMAT_VERSION:
331 raise self.ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
332 if pickle_data['JAR_MD5SUM'] != jar_md5:
333 raise self.ProguardPickleException('JAR file MD5 sum differs.')
334 return pickle_data['TEST_METHODS']
335 except TypeError as e:
336 logging.error(pickle_data)
337 raise self.ProguardPickleException(str(e))
338
339 def _GetTestsFromProguard(self, jar_path):
340 p = proguard.Dump(jar_path)
341
342 def is_test_class(c):
343 return c['class'].endswith('Test')
344
345 def is_test_method(m):
346 return m['method'].startswith('test')
347
348 class_lookup = dict((c['class'], c) for c in p['classes'])
349 def recursive_get_class_annotations(c):
350 s = c['superclass']
351 if s in class_lookup:
352 a = recursive_get_class_annotations(class_lookup[s])
353 else:
354 a = {}
355 a.update(c['annotations'])
356 return a
357
358 def stripped_test_class(c):
359 return {
360 'class': c['class'],
361 'annotations': recursive_get_class_annotations(c),
362 'methods': [m for m in c['methods'] if is_test_method(m)],
363 }
364
365 return [stripped_test_class(c) for c in p['classes']
366 if is_test_class(c)]
367
368 def _SaveTestsToPickle(self, pickle_path, jar_path, tests):
369 jar_md5, _ = md5sum.CalculateHostMd5Sums(jar_path)[0]
370 pickle_data = {
371 'VERSION': _PICKLE_FORMAT_VERSION,
372 'JAR_MD5SUM': jar_md5,
373 'TEST_METHODS': tests,
374 }
375 with open(pickle_path, 'w') as pickle_file:
376 pickle.dump(pickle_data, pickle_file)
377
378 def _FilterTests(self, tests):
379
380 def gtest_filter(c, m):
381 t = ['%s.%s' % (c['class'].split('.')[-1], m['method'])]
382 return (not self._test_filter
383 or unittest_util.FilterTestNames(t, self._test_filter))
384
385 def annotation_filter(all_annotations):
386 if not self._annotations:
387 return True
388 return any_annotation_matches(self._annotations, all_annotations)
389
390 def excluded_annotation_filter(all_annotations):
391 if not self._excluded_annotations:
392 return True
393 return not any_annotation_matches(self._excluded_annotations,
394 all_annotations)
395
396 def any_annotation_matches(annotations, all_annotations):
397 return any(
398 ak in all_annotations and (av is None or av == all_annotations[ak])
399 for ak, av in annotations.iteritems())
400
401 filtered_classes = []
402 for c in tests:
403 filtered_methods = []
404 for m in c['methods']:
405 # Gtest filtering
406 if not gtest_filter(c, m):
407 continue
408
409 all_annotations = dict(c['annotations'])
410 all_annotations.update(m['annotations'])
411 if (not annotation_filter(all_annotations)
412 or not excluded_annotation_filter(all_annotations)):
413 continue
414
415 filtered_methods.append(m)
416
417 if filtered_methods:
418 filtered_class = dict(c)
419 filtered_class['methods'] = filtered_methods
420 filtered_classes.append(filtered_class)
421
422 return filtered_classes
423
424 def _InflateTests(self, tests):
425 inflated_tests = []
426 for c in tests:
427 for m in c['methods']:
428 a = dict(c['annotations'])
429 a.update(m['annotations'])
430 inflated_tests.append({
431 'class': c['class'],
432 'method': m['method'],
433 'annotations': a,
434 })
435 return inflated_tests
436
437 @staticmethod
438 def ParseAmInstrumentRawOutput(raw_output):
439 return ParseAmInstrumentRawOutput(raw_output)
440
441 @staticmethod
442 def GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms):
443 return GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms)
444
445 #override
446 def TearDown(self):
447 if self._isolate_delegate:
448 self._isolate_delegate.Clear()
449
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698