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 pylib
import cmd_helper
15 from pylib
import constants
16 from pylib
.device
import device_utils
17 from pylib
.utils
import md5sum
18 from pylib
.utils
import proguard
21 os
.path
.join(constants
.DIR_SOURCE_ROOT
,
22 'build', 'util', 'lib', 'common'))
24 import unittest_util
# pylint: disable=F0401
26 # If you change the cached output of proguard, increment this number
27 PICKLE_FORMAT_VERSION
= 4
30 class TestJar(object):
31 _ANNOTATIONS
= frozenset(
32 ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest',
33 'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest',
35 _DEFAULT_ANNOTATION
= 'SmallTest'
36 _PROGUARD_CLASS_RE
= re
.compile(r
'\s*?- Program class:\s*([\S]+)$')
37 _PROGUARD_SUPERCLASS_RE
= re
.compile(r
'\s*? Superclass:\s*([\S]+)$')
38 _PROGUARD_METHOD_RE
= re
.compile(r
'\s*?- Method:\s*(\S*)[(].*$')
39 _PROGUARD_ANNOTATION_RE
= re
.compile(r
'\s*?- Annotation \[L(\S*);\]:$')
40 _PROGUARD_ANNOTATION_CONST_RE
= (
41 re
.compile(r
'\s*?- Constant element value.*$'))
42 _PROGUARD_ANNOTATION_VALUE_RE
= re
.compile(r
'\s*?- \S+? \[(.*)\]$')
44 def __init__(self
, jar_path
):
45 if not os
.path
.exists(jar_path
):
46 raise Exception('%s not found, please build it' % jar_path
)
48 self
._PROGUARD
_PATH
= os
.path
.join(constants
.ANDROID_SDK_ROOT
,
49 'tools/proguard/lib/proguard.jar')
50 if not os
.path
.exists(self
._PROGUARD
_PATH
):
51 self
._PROGUARD
_PATH
= os
.path
.join(os
.environ
['ANDROID_BUILD_TOP'],
52 'external/proguard/lib/proguard.jar')
53 self
._jar
_path
= jar_path
54 self
._pickled
_proguard
_name
= self
._jar
_path
+ '-proguard.pickle'
55 self
._test
_methods
= {}
56 if not self
._GetCachedProguardData
():
57 self
._GetProguardData
()
59 def _GetCachedProguardData(self
):
60 if (os
.path
.exists(self
._pickled
_proguard
_name
) and
61 (os
.path
.getmtime(self
._pickled
_proguard
_name
) >
62 os
.path
.getmtime(self
._jar
_path
))):
63 logging
.info('Loading cached proguard output from %s',
64 self
._pickled
_proguard
_name
)
66 with
open(self
._pickled
_proguard
_name
, 'r') as r
:
67 d
= pickle
.loads(r
.read())
68 jar_md5
= md5sum
.CalculateHostMd5Sums(self
._jar
_path
)[0].hash
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
,
108 'JAR_MD5SUM': md5sum
.CalculateHostMd5Sums(self
._jar
_path
)[0].hash}
109 with
open(self
._pickled
_proguard
_name
, 'w') as f
:
110 f
.write(pickle
.dumps(d
))
113 def _IsTestMethod(test
):
114 class_name
, method
= test
.split('#')
115 return class_name
.endswith('Test') and method
.startswith('test')
117 def GetTestAnnotations(self
, test
):
118 """Returns a list of all annotations for the given |test|. May be empty."""
119 if not self
._IsTestMethod
(test
) or not test
in self
._test
_methods
:
121 return self
._test
_methods
[test
]['annotations']
124 def _AnnotationsMatchFilters(annotation_filter_list
, annotations
):
125 """Checks if annotations match any of the filters."""
126 if not annotation_filter_list
:
128 for annotation_filter
in annotation_filter_list
:
129 filters
= annotation_filter
.split('=')
130 if len(filters
) == 2:
132 value_list
= filters
[1].split(',')
133 for value
in value_list
:
134 if key
in annotations
and value
== annotations
[key
]:
136 elif annotation_filter
in annotations
:
140 def GetAnnotatedTests(self
, annotation_filter_list
):
141 """Returns a list of all tests that match the given annotation filters."""
142 return [test
for test
in self
.GetTestMethods()
143 if self
._IsTestMethod
(test
) and self
._AnnotationsMatchFilters
(
144 annotation_filter_list
, self
.GetTestAnnotations(test
))]
146 def GetTestMethods(self
):
147 """Returns a dict of all test methods and relevant attributes.
149 Test methods are retrieved as Class#testMethod.
151 return self
._test
_methods
153 def _GetTestsMissingAnnotation(self
):
154 """Get a list of test methods with no known annotations."""
155 tests_missing_annotations
= []
156 for test_method
in self
.GetTestMethods().iterkeys():
157 annotations_
= frozenset(self
.GetTestAnnotations(test_method
).iterkeys())
158 if (annotations_
.isdisjoint(self
._ANNOTATIONS
) and
159 not self
.IsHostDrivenTest(test_method
)):
160 tests_missing_annotations
.append(test_method
)
161 return sorted(tests_missing_annotations
)
163 def _IsTestValidForSdkRange(self
, test_name
, attached_min_sdk_level
):
164 required_min_sdk_level
= int(
165 self
.GetTestAnnotations(test_name
).get('MinAndroidSdkLevel', 0))
166 return (required_min_sdk_level
is None or
167 attached_min_sdk_level
>= required_min_sdk_level
)
169 def GetAllMatchingTests(self
, annotation_filter_list
,
170 exclude_annotation_list
, test_filter
):
171 """Get a list of tests matching any of the annotations and the filter.
174 annotation_filter_list: List of test annotations. A test must have at
175 least one of these annotations. A test without any annotations is
176 considered to be SmallTest.
177 exclude_annotation_list: List of test annotations. A test must not have
178 any of these annotations.
179 test_filter: Filter used for partial matching on the test method names.
182 List of all matching tests.
184 if annotation_filter_list
:
185 available_tests
= self
.GetAnnotatedTests(annotation_filter_list
)
186 # Include un-annotated tests in SmallTest.
187 if annotation_filter_list
.count(self
._DEFAULT
_ANNOTATION
) > 0:
188 for test
in self
._GetTestsMissingAnnotation
():
190 '%s has no annotations. Assuming "%s".', test
,
191 self
._DEFAULT
_ANNOTATION
)
192 available_tests
.append(test
)
194 available_tests
= [m
for m
in self
.GetTestMethods()
195 if not self
.IsHostDrivenTest(m
)]
197 if exclude_annotation_list
:
198 excluded_tests
= self
.GetAnnotatedTests(exclude_annotation_list
)
199 available_tests
= list(set(available_tests
) - set(excluded_tests
))
203 # |available_tests| are in adb instrument format: package.path.class#test.
205 # Maps a 'class.test' name to each 'package.path.class#test' name.
206 sanitized_test_names
= dict([
207 (t
.split('.')[-1].replace('#', '.'), t
) for t
in available_tests
])
208 # Filters 'class.test' names and populates |tests| with the corresponding
209 # 'package.path.class#test' names.
211 sanitized_test_names
[t
] for t
in unittest_util
.FilterTestNames(
212 sanitized_test_names
.keys(), test_filter
.replace('#', '.'))]
214 tests
= available_tests
216 # Filter out any tests with SDK level requirements that don't match the set
217 # of attached devices.
218 devices
= device_utils
.DeviceUtils
.parallel()
219 min_sdk_version
= min(devices
.build_version_sdk
.pGet(None))
220 tests
= [t
for t
in tests
221 if self
._IsTestValidForSdkRange
(t
, min_sdk_version
)]
226 def IsHostDrivenTest(test
):
227 return 'pythonDrivenTests' in test