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.
11 from pylib
import cmd_helper
12 from pylib
import constants
13 from pylib
import flag_changer
14 from pylib
.base
import base_test_result
15 from pylib
.base
import test_instance
16 from pylib
.instrumentation
import test_result
17 from pylib
.instrumentation
import instrumentation_parser
18 from pylib
.utils
import apk_helper
19 from pylib
.utils
import md5sum
20 from pylib
.utils
import proguard
23 os
.path
.join(constants
.DIR_SOURCE_ROOT
, 'build', 'util', 'lib', 'common'))
26 # Ref: http://developer.android.com/reference/android/app/Activity.html
27 _ACTIVITY_RESULT_CANCELED
= 0
28 _ACTIVITY_RESULT_OK
= -1
30 _DEFAULT_ANNOTATIONS
= [
31 'Smoke', 'SmallTest', 'MediumTest', 'LargeTest',
32 'EnormousTest', 'IntegrationTest']
33 _NATIVE_CRASH_RE
= re
.compile('native crash', re
.IGNORECASE
)
34 _PICKLE_FORMAT_VERSION
= 10
37 # TODO(jbudorick): Make these private class methods of
38 # InstrumentationTestInstance once the instrumentation test_runner is
40 def ParseAmInstrumentRawOutput(raw_output
):
41 """Parses the output of an |am instrument -r| call.
44 raw_output: the output of an |am instrument -r| call as a list of lines
47 - the instrumentation code as an integer
48 - the instrumentation result as a list of lines
49 - the instrumentation statuses received as a list of 2-tuples
51 - the status code as an integer
52 - the bundle dump as a dict mapping string keys to a list of
53 strings, one for each line.
55 parser
= instrumentation_parser
.InstrumentationParser(raw_output
)
56 statuses
= list(parser
.IterStatus())
57 code
, bundle
= parser
.GetResult()
58 return (code
, bundle
, statuses
)
61 def GenerateTestResults(
62 result_code
, result_bundle
, statuses
, start_ms
, duration_ms
):
63 """Generate test results from |statuses|.
66 result_code: The overall status code as an integer.
67 result_bundle: The summary bundle dump as a dict.
68 statuses: A list of 2-tuples containing:
69 - the status code as an integer
70 - the bundle dump as a dict mapping string keys to string values
71 Note that this is the same as the third item in the 3-tuple returned by
72 |_ParseAmInstrumentRawOutput|.
73 start_ms: The start time of the test in milliseconds.
74 duration_ms: The duration of the test in milliseconds.
77 A list containing an instance of InstrumentationTestResult for each test
85 for status_code
, bundle
in statuses
:
86 test_class
= bundle
.get('class', '')
87 test_method
= bundle
.get('test', '')
88 if test_class
and test_method
:
89 test_name
= '%s#%s' % (test_class
, test_method
)
93 if status_code
== instrumentation_parser
.STATUS_CODE_START
:
95 results
.append(current_result
)
96 current_result
= test_result
.InstrumentationTestResult(
97 test_name
, base_test_result
.ResultType
.UNKNOWN
, start_ms
, duration_ms
)
99 if status_code
== instrumentation_parser
.STATUS_CODE_OK
:
100 if bundle
.get('test_skipped', '').lower() in ('true', '1', 'yes'):
101 current_result
.SetType(base_test_result
.ResultType
.SKIP
)
102 elif current_result
.GetType() == base_test_result
.ResultType
.UNKNOWN
:
103 current_result
.SetType(base_test_result
.ResultType
.PASS
)
105 if status_code
not in (instrumentation_parser
.STATUS_CODE_ERROR
,
106 instrumentation_parser
.STATUS_CODE_FAILURE
):
107 logging
.error('Unrecognized status code %d. Handling as an error.',
109 current_result
.SetType(base_test_result
.ResultType
.FAIL
)
110 if 'stack' in bundle
:
111 current_result
.SetLog(bundle
['stack'])
114 if current_result
.GetType() == base_test_result
.ResultType
.UNKNOWN
:
115 crashed
= (result_code
== _ACTIVITY_RESULT_CANCELED
116 and any(_NATIVE_CRASH_RE
.search(l
)
117 for l
in result_bundle
.itervalues()))
119 current_result
.SetType(base_test_result
.ResultType
.CRASH
)
121 results
.append(current_result
)
126 class InstrumentationTestInstance(test_instance
.TestInstance
):
128 def __init__(self
, args
, isolate_delegate
, error_func
):
129 super(InstrumentationTestInstance
, self
).__init
__()
131 self
._apk
_under
_test
= None
132 self
._package
_info
= None
133 self
._test
_apk
= None
134 self
._test
_jar
= None
135 self
._test
_package
= None
136 self
._test
_runner
= None
137 self
._test
_support
_apk
= None
138 self
.__initializeApkAttributes
(args
, error_func
)
140 self
._data
_deps
= None
141 self
._isolate
_abs
_path
= None
142 self
._isolate
_delegate
= None
143 self
._isolated
_abs
_path
= None
144 self
._test
_data
= None
145 self
.__initializeDataDependencyAttributes
(args
, isolate_delegate
)
147 self
._annotations
= None
148 self
._excluded
_annotations
= None
149 self
._test
_filter
= None
150 self
.__initializeTestFilterAttributes
(args
)
153 self
.__initializeFlagAttributes
(args
)
155 def __initializeApkAttributes(self
, args
, error_func
):
156 if args
.apk_under_test
.endswith('.apk'):
157 self
._apk
_under
_test
= args
.apk_under_test
159 self
._apk
_under
_test
= os
.path
.join(
160 constants
.GetOutDirectory(), constants
.SDK_BUILD_APKS_DIR
,
161 '%s.apk' % args
.apk_under_test
)
163 if not os
.path
.exists(self
._apk
_under
_test
):
164 error_func('Unable to find APK under test: %s' % self
._apk
_under
_test
)
166 if args
.test_apk
.endswith('.apk'):
167 test_apk_root
= os
.path
.splitext(os
.path
.basename(args
.test_apk
))[0]
168 self
._test
_apk
= args
.test_apk
170 test_apk_root
= args
.test_apk
171 self
._test
_apk
= os
.path
.join(
172 constants
.GetOutDirectory(), constants
.SDK_BUILD_APKS_DIR
,
173 '%s.apk' % args
.test_apk
)
175 self
._test
_jar
= os
.path
.join(
176 constants
.GetOutDirectory(), constants
.SDK_BUILD_TEST_JAVALIB_DIR
,
177 '%s.jar' % test_apk_root
)
178 self
._test
_support
_apk
= os
.path
.join(
179 constants
.GetOutDirectory(), constants
.SDK_BUILD_TEST_JAVALIB_DIR
,
180 '%sSupport.apk' % test_apk_root
)
182 if not os
.path
.exists(self
._test
_apk
):
183 error_func('Unable to find test APK: %s' % self
._test
_apk
)
184 if not os
.path
.exists(self
._test
_jar
):
185 error_func('Unable to find test JAR: %s' % self
._test
_jar
)
187 self
._test
_package
= apk_helper
.GetPackageName(self
.test_apk
)
188 self
._test
_runner
= apk_helper
.GetInstrumentationName(self
.test_apk
)
190 self
._package
_info
= None
191 for package_info
in constants
.PACKAGE_INFO
.itervalues():
192 if self
._test
_package
== package_info
.test_package
:
193 self
._package
_info
= package_info
194 if not self
._package
_info
:
195 logging
.warning('Unable to find package info for %s', self
._test
_package
)
197 def __initializeDataDependencyAttributes(self
, args
, isolate_delegate
):
199 if args
.isolate_file_path
:
200 self
._isolate
_abs
_path
= os
.path
.abspath(args
.isolate_file_path
)
201 self
._isolate
_delegate
= isolate_delegate
202 self
._isolated
_abs
_path
= os
.path
.join(
203 constants
.GetOutDirectory(), '%s.isolated' % self
._test
_package
)
205 self
._isolate
_delegate
= None
207 # TODO(jbudorick): Deprecate and remove --test-data once data dependencies
208 # are fully converted to isolate.
210 logging
.info('Data dependencies specified via --test-data')
211 self
._test
_data
= args
.test_data
213 self
._test
_data
= None
215 if not self
._isolate
_delegate
and not self
._test
_data
:
216 logging
.warning('No data dependencies will be pushed.')
218 def __initializeTestFilterAttributes(self
, args
):
219 self
._test
_filter
= args
.test_filter
221 def annotation_dict_element(a
):
223 return (a
[0], a
[1] if len(a
) == 2 else None)
225 if args
.annotation_str
:
226 self
._annotations
= dict(
227 annotation_dict_element(a
)
228 for a
in args
.annotation_str
.split(','))
229 elif not self
._test
_filter
:
230 self
._annotations
= dict(
231 annotation_dict_element(a
)
232 for a
in _DEFAULT_ANNOTATIONS
)
234 self
._annotations
= {}
236 if args
.exclude_annotation_str
:
237 self
._excluded
_annotations
= dict(
238 annotation_dict_element(a
)
239 for a
in args
.exclude_annotation_str
.split(','))
241 self
._excluded
_annotations
= {}
243 def __initializeFlagAttributes(self
, args
):
244 self
._flags
= ['--disable-fre', '--enable-test-intents']
245 # TODO(jbudorick): Transition "--device-flags" to "--device-flags-file"
246 if hasattr(args
, 'device_flags') and args
.device_flags
:
247 with
open(args
.device_flags
) as device_flags_file
:
248 stripped_lines
= (l
.strip() for l
in device_flags_file
)
249 self
._flags
.extend([flag
for flag
in stripped_lines
if flag
])
250 if hasattr(args
, 'device_flags_file') and args
.device_flags_file
:
251 with
open(args
.device_flags_file
) as device_flags_file
:
252 stripped_lines
= (l
.strip() for l
in device_flags_file
)
253 self
._flags
.extend([flag
for flag
in stripped_lines
if flag
])
257 return 'instrumentation'
260 def apk_under_test(self
):
261 return self
._apk
_under
_test
268 def package_info(self
):
269 return self
._package
_info
273 return self
._test
_apk
277 return self
._test
_jar
280 def test_support_apk(self
):
281 return self
._test
_support
_apk
284 def test_package(self
):
285 return self
._test
_package
288 def test_runner(self
):
289 return self
._test
_runner
293 return 'instrumentation'
297 if self
._isolate
_delegate
:
298 self
._isolate
_delegate
.Remap(
299 self
._isolate
_abs
_path
, self
._isolated
_abs
_path
)
300 self
._isolate
_delegate
.MoveOutputDeps()
301 self
._data
_deps
.extend([(constants
.ISOLATE_DEPS_DIR
, None)])
303 # TODO(jbudorick): Convert existing tests that depend on the --test-data
304 # mechanism to isolate, then remove this.
306 for t
in self
._test
_data
:
307 device_rel_path
, host_rel_path
= t
.split(':')
308 host_abs_path
= os
.path
.join(constants
.DIR_SOURCE_ROOT
, host_rel_path
)
309 self
._data
_deps
.extend(
311 [None, 'chrome', 'test', 'data', device_rel_path
])])
313 def GetDataDependencies(self
):
314 return self
._data
_deps
317 pickle_path
= '%s-proguard.pickle' % self
.test_jar
319 tests
= self
._GetTestsFromPickle
(pickle_path
, self
.test_jar
)
320 except self
.ProguardPickleException
as e
:
321 logging
.info('Getting tests from JAR via proguard. (%s)' % str(e
))
322 tests
= self
._GetTestsFromProguard
(self
.test_jar
)
323 self
._SaveTestsToPickle
(pickle_path
, self
.test_jar
, tests
)
324 return self
._InflateTests
(self
._FilterTests
(tests
))
326 class ProguardPickleException(Exception):
329 def _GetTestsFromPickle(self
, pickle_path
, jar_path
):
330 if not os
.path
.exists(pickle_path
):
331 raise self
.ProguardPickleException('%s does not exist.' % pickle_path
)
332 if os
.path
.getmtime(pickle_path
) <= os
.path
.getmtime(jar_path
):
333 raise self
.ProguardPickleException(
334 '%s newer than %s.' % (jar_path
, pickle_path
))
336 with
open(pickle_path
, 'r') as pickle_file
:
337 pickle_data
= pickle
.loads(pickle_file
.read())
338 jar_md5
, _
= md5sum
.CalculateHostMd5Sums(jar_path
)[0]
341 if pickle_data
['VERSION'] != _PICKLE_FORMAT_VERSION
:
342 raise self
.ProguardPickleException('PICKLE_FORMAT_VERSION has changed.')
343 if pickle_data
['JAR_MD5SUM'] != jar_md5
:
344 raise self
.ProguardPickleException('JAR file MD5 sum differs.')
345 return pickle_data
['TEST_METHODS']
346 except TypeError as e
:
347 logging
.error(pickle_data
)
348 raise self
.ProguardPickleException(str(e
))
350 def _GetTestsFromProguard(self
, jar_path
):
351 p
= proguard
.Dump(jar_path
)
353 def is_test_class(c
):
354 return c
['class'].endswith('Test')
356 def is_test_method(m
):
357 return m
['method'].startswith('test')
359 class_lookup
= dict((c
['class'], c
) for c
in p
['classes'])
360 def recursive_get_class_annotations(c
):
362 if s
in class_lookup
:
363 a
= recursive_get_class_annotations(class_lookup
[s
])
366 a
.update(c
['annotations'])
369 def stripped_test_class(c
):
372 'annotations': recursive_get_class_annotations(c
),
373 'methods': [m
for m
in c
['methods'] if is_test_method(m
)],
376 return [stripped_test_class(c
) for c
in p
['classes']
379 def _SaveTestsToPickle(self
, pickle_path
, jar_path
, tests
):
380 jar_md5
, _
= md5sum
.CalculateHostMd5Sums(jar_path
)[0]
382 'VERSION': _PICKLE_FORMAT_VERSION
,
383 'JAR_MD5SUM': jar_md5
,
384 'TEST_METHODS': tests
,
386 with
open(pickle_path
, 'w') as pickle_file
:
387 pickle
.dump(pickle_data
, pickle_file
)
389 def _FilterTests(self
, tests
):
391 def gtest_filter(c
, m
):
392 t
= ['%s.%s' % (c
['class'].split('.')[-1], m
['method'])]
393 return (not self
._test
_filter
394 or unittest_util
.FilterTestNames(t
, self
._test
_filter
))
396 def annotation_filter(all_annotations
):
397 if not self
._annotations
:
399 return any_annotation_matches(self
._annotations
, all_annotations
)
401 def excluded_annotation_filter(all_annotations
):
402 if not self
._excluded
_annotations
:
404 return not any_annotation_matches(self
._excluded
_annotations
,
407 def any_annotation_matches(annotations
, all_annotations
):
409 ak
in all_annotations
and (av
is None or av
== all_annotations
[ak
])
410 for ak
, av
in annotations
.iteritems())
412 filtered_classes
= []
414 filtered_methods
= []
415 for m
in c
['methods']:
417 if not gtest_filter(c
, m
):
420 all_annotations
= dict(c
['annotations'])
421 all_annotations
.update(m
['annotations'])
422 if (not annotation_filter(all_annotations
)
423 or not excluded_annotation_filter(all_annotations
)):
426 filtered_methods
.append(m
)
429 filtered_class
= dict(c
)
430 filtered_class
['methods'] = filtered_methods
431 filtered_classes
.append(filtered_class
)
433 return filtered_classes
435 def _InflateTests(self
, tests
):
438 for m
in c
['methods']:
439 a
= dict(c
['annotations'])
440 a
.update(m
['annotations'])
441 inflated_tests
.append({
443 'method': m
['method'],
446 return inflated_tests
449 def ParseAmInstrumentRawOutput(raw_output
):
450 return ParseAmInstrumentRawOutput(raw_output
)
453 def GenerateTestResults(
454 result_code
, result_bundle
, statuses
, start_ms
, duration_ms
):
455 return GenerateTestResults(result_code
, result_bundle
, statuses
,
456 start_ms
, duration_ms
)
460 if self
._isolate
_delegate
:
461 self
._isolate
_delegate
.Clear()