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

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

Powered by Google App Engine
This is Rietveld 408576698