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
3 # found in the LICENSE file.
5 """Helper class for instrumenation test jar."""
6 # pylint: disable=W0702
14 from devil
.android
import device_utils
15 from devil
.android
import md5sum
16 from pylib
import constants
17 from pylib
.utils
import proguard
20 os
.path
.join(constants
.DIR_SOURCE_ROOT
,
21 'build', 'util', 'lib', 'common'))
23 import unittest_util
# pylint: disable=F0401
25 # If you change the cached output of proguard, increment this number
26 PICKLE_FORMAT_VERSION
= 4
29 class TestJar(object):
30 _ANNOTATIONS
= frozenset(
31 ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest',
32 'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest',
34 _DEFAULT_ANNOTATION
= 'SmallTest'
35 _PROGUARD_CLASS_RE
= re
.compile(r
'\s*?- Program class:\s*([\S]+)$')
36 _PROGUARD_SUPERCLASS_RE
= re
.compile(r
'\s*? Superclass:\s*([\S]+)$')
37 _PROGUARD_METHOD_RE
= re
.compile(r
'\s*?- Method:\s*(\S*)[(].*$')
38 _PROGUARD_ANNOTATION_RE
= re
.compile(r
'\s*?- Annotation \[L(\S*);\]:$')
39 _PROGUARD_ANNOTATION_CONST_RE
= (
40 re
.compile(r
'\s*?- Constant element value.*$'))
41 _PROGUARD_ANNOTATION_VALUE_RE
= re
.compile(r
'\s*?- \S+? \[(.*)\]$')
43 def __init__(self
, jar_path
):
44 if not os
.path
.exists(jar_path
):
45 raise Exception('%s not found, please build it' % jar_path
)
47 self
._PROGUARD
_PATH
= os
.path
.join(constants
.PROGUARD_ROOT
,
48 'lib', 'proguard.jar')
49 if not os
.path
.exists(self
._PROGUARD
_PATH
):
50 self
._PROGUARD
_PATH
= os
.path
.join(os
.environ
['ANDROID_BUILD_TOP'],
51 'external/proguard/lib/proguard.jar')
52 self
._jar
_path
= jar_path
53 self
._pickled
_proguard
_name
= self
._jar
_path
+ '-proguard.pickle'
54 self
._test
_methods
= {}
55 if not self
._GetCachedProguardData
():
56 self
._GetProguardData
()
58 def _GetCachedProguardData(self
):
59 if (os
.path
.exists(self
._pickled
_proguard
_name
) and
60 (os
.path
.getmtime(self
._pickled
_proguard
_name
) >
61 os
.path
.getmtime(self
._jar
_path
))):
62 logging
.info('Loading cached proguard output from %s',
63 self
._pickled
_proguard
_name
)
65 with
open(self
._pickled
_proguard
_name
, 'r') as r
:
66 d
= pickle
.loads(r
.read())
67 jar_md5
= md5sum
.CalculateHostMd5Sums(
68 self
._jar
_path
)[os
.path
.realpath(self
._jar
_path
)]
69 if (d
['JAR_MD5SUM'] == jar_md5
and
70 d
['VERSION'] == PICKLE_FORMAT_VERSION
):
71 self
._test
_methods
= d
['TEST_METHODS']
74 logging
.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache')
77 def _GetProguardData(self
):
78 logging
.info('Retrieving test methods via proguard.')
80 p
= proguard
.Dump(self
._jar
_path
)
82 class_lookup
= dict((c
['class'], c
) for c
in p
['classes'])
83 def recursive_get_annotations(c
):
86 a
= recursive_get_annotations(class_lookup
[s
])
89 a
.update(c
['annotations'])
92 test_classes
= (c
for c
in p
['classes']
93 if c
['class'].endswith('Test'))
94 for c
in test_classes
:
95 class_annotations
= recursive_get_annotations(c
)
96 test_methods
= (m
for m
in c
['methods']
97 if m
['method'].startswith('test'))
98 for m
in test_methods
:
99 qualified_method
= '%s#%s' % (c
['class'], m
['method'])
100 annotations
= dict(class_annotations
)
101 annotations
.update(m
['annotations'])
102 self
._test
_methods
[qualified_method
] = m
103 self
._test
_methods
[qualified_method
]['annotations'] = annotations
105 logging
.info('Storing proguard output to %s', self
._pickled
_proguard
_name
)
106 d
= {'VERSION': PICKLE_FORMAT_VERSION
,
107 'TEST_METHODS': self
._test
_methods
,
109 md5sum
.CalculateHostMd5Sums(
110 self
._jar
_path
)[os
.path
.realpath(self
._jar
_path
)]}
111 with
open(self
._pickled
_proguard
_name
, 'w') as f
:
112 f
.write(pickle
.dumps(d
))
115 def _IsTestMethod(test
):
116 class_name
, method
= test
.split('#')
117 return class_name
.endswith('Test') and method
.startswith('test')
119 def GetTestAnnotations(self
, test
):
120 """Returns a list of all annotations for the given |test|. May be empty."""
121 if not self
._IsTestMethod
(test
) or not test
in self
._test
_methods
:
123 return self
._test
_methods
[test
]['annotations']
126 def _AnnotationsMatchFilters(annotation_filter_list
, annotations
):
127 """Checks if annotations match any of the filters."""
128 if not annotation_filter_list
:
130 for annotation_filter
in annotation_filter_list
:
131 filters
= annotation_filter
.split('=')
132 if len(filters
) == 2:
134 value_list
= filters
[1].split(',')
135 for value
in value_list
:
136 if key
in annotations
and value
== annotations
[key
]:
138 elif annotation_filter
in annotations
:
142 def GetAnnotatedTests(self
, annotation_filter_list
):
143 """Returns a list of all tests that match the given annotation filters."""
144 return [test
for test
in self
.GetTestMethods()
145 if self
._IsTestMethod
(test
) and self
._AnnotationsMatchFilters
(
146 annotation_filter_list
, self
.GetTestAnnotations(test
))]
148 def GetTestMethods(self
):
149 """Returns a dict of all test methods and relevant attributes.
151 Test methods are retrieved as Class#testMethod.
153 return self
._test
_methods
155 def _GetTestsMissingAnnotation(self
):
156 """Get a list of test methods with no known annotations."""
157 tests_missing_annotations
= []
158 for test_method
in self
.GetTestMethods().iterkeys():
159 annotations_
= frozenset(self
.GetTestAnnotations(test_method
).iterkeys())
160 if (annotations_
.isdisjoint(self
._ANNOTATIONS
) and
161 not self
.IsHostDrivenTest(test_method
)):
162 tests_missing_annotations
.append(test_method
)
163 return sorted(tests_missing_annotations
)
165 def _IsTestValidForSdkRange(self
, test_name
, attached_min_sdk_level
):
166 required_min_sdk_level
= int(
167 self
.GetTestAnnotations(test_name
).get('MinAndroidSdkLevel', 0))
168 return (required_min_sdk_level
is None or
169 attached_min_sdk_level
>= required_min_sdk_level
)
171 def GetAllMatchingTests(self
, annotation_filter_list
,
172 exclude_annotation_list
, test_filter
, devices
):
173 """Get a list of tests matching any of the annotations and the filter.
176 annotation_filter_list: List of test annotations. A test must have at
177 least one of these annotations. A test without any annotations is
178 considered to be SmallTest.
179 exclude_annotation_list: List of test annotations. A test must not have
180 any of these annotations.
181 test_filter: Filter used for partial matching on the test method names.
182 devices: The set of devices against which tests will be run.
185 List of all matching tests.
187 if annotation_filter_list
:
188 available_tests
= self
.GetAnnotatedTests(annotation_filter_list
)
189 # Include un-annotated tests in SmallTest.
190 if annotation_filter_list
.count(self
._DEFAULT
_ANNOTATION
) > 0:
191 for test
in self
._GetTestsMissingAnnotation
():
193 '%s has no annotations. Assuming "%s".', test
,
194 self
._DEFAULT
_ANNOTATION
)
195 available_tests
.append(test
)
197 available_tests
= [m
for m
in self
.GetTestMethods()
198 if not self
.IsHostDrivenTest(m
)]
200 if exclude_annotation_list
:
201 excluded_tests
= self
.GetAnnotatedTests(exclude_annotation_list
)
202 available_tests
= list(set(available_tests
) - set(excluded_tests
))
206 # |available_tests| are in adb instrument format: package.path.class#test.
208 # Maps a 'class.test' name to each 'package.path.class#test' name.
209 sanitized_test_names
= dict([
210 (t
.split('.')[-1].replace('#', '.'), t
) for t
in available_tests
])
211 # Filters 'class.test' names and populates |tests| with the corresponding
212 # 'package.path.class#test' names.
214 sanitized_test_names
[t
] for t
in unittest_util
.FilterTestNames(
215 sanitized_test_names
.keys(), test_filter
.replace('#', '.'))]
217 tests
= available_tests
219 # Filter out any tests with SDK level requirements that don't match the set
220 # of attached devices.
221 devices
= device_utils
.DeviceUtils
.parallel(devices
)
222 min_sdk_version
= min(devices
.build_version_sdk
.pGet(None))
223 tests
= [t
for t
in tests
224 if self
._IsTestValidForSdkRange
(t
, min_sdk_version
)]
229 def IsHostDrivenTest(test
):
230 return 'pythonDrivenTests' in test