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
)[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(self
._jar
_path
)[self
._jar
_path
]}
110 with
open(self
._pickled
_proguard
_name
, 'w') as f
:
111 f
.write(pickle
.dumps(d
))
114 def _IsTestMethod(test
):
115 class_name
, method
= test
.split('#')
116 return class_name
.endswith('Test') and method
.startswith('test')
118 def GetTestAnnotations(self
, test
):
119 """Returns a list of all annotations for the given |test|. May be empty."""
120 if not self
._IsTestMethod
(test
) or not test
in self
._test
_methods
:
122 return self
._test
_methods
[test
]['annotations']
125 def _AnnotationsMatchFilters(annotation_filter_list
, annotations
):
126 """Checks if annotations match any of the filters."""
127 if not annotation_filter_list
:
129 for annotation_filter
in annotation_filter_list
:
130 filters
= annotation_filter
.split('=')
131 if len(filters
) == 2:
133 value_list
= filters
[1].split(',')
134 for value
in value_list
:
135 if key
in annotations
and value
== annotations
[key
]:
137 elif annotation_filter
in annotations
:
141 def GetAnnotatedTests(self
, annotation_filter_list
):
142 """Returns a list of all tests that match the given annotation filters."""
143 return [test
for test
in self
.GetTestMethods()
144 if self
._IsTestMethod
(test
) and self
._AnnotationsMatchFilters
(
145 annotation_filter_list
, self
.GetTestAnnotations(test
))]
147 def GetTestMethods(self
):
148 """Returns a dict of all test methods and relevant attributes.
150 Test methods are retrieved as Class#testMethod.
152 return self
._test
_methods
154 def _GetTestsMissingAnnotation(self
):
155 """Get a list of test methods with no known annotations."""
156 tests_missing_annotations
= []
157 for test_method
in self
.GetTestMethods().iterkeys():
158 annotations_
= frozenset(self
.GetTestAnnotations(test_method
).iterkeys())
159 if (annotations_
.isdisjoint(self
._ANNOTATIONS
) and
160 not self
.IsHostDrivenTest(test_method
)):
161 tests_missing_annotations
.append(test_method
)
162 return sorted(tests_missing_annotations
)
164 def _IsTestValidForSdkRange(self
, test_name
, attached_min_sdk_level
):
165 required_min_sdk_level
= int(
166 self
.GetTestAnnotations(test_name
).get('MinAndroidSdkLevel', 0))
167 return (required_min_sdk_level
is None or
168 attached_min_sdk_level
>= required_min_sdk_level
)
170 def GetAllMatchingTests(self
, annotation_filter_list
,
171 exclude_annotation_list
, test_filter
):
172 """Get a list of tests matching any of the annotations and the filter.
175 annotation_filter_list: List of test annotations. A test must have at
176 least one of these annotations. A test without any annotations is
177 considered to be SmallTest.
178 exclude_annotation_list: List of test annotations. A test must not have
179 any of these annotations.
180 test_filter: Filter used for partial matching on the test method names.
183 List of all matching tests.
185 if annotation_filter_list
:
186 available_tests
= self
.GetAnnotatedTests(annotation_filter_list
)
187 # Include un-annotated tests in SmallTest.
188 if annotation_filter_list
.count(self
._DEFAULT
_ANNOTATION
) > 0:
189 for test
in self
._GetTestsMissingAnnotation
():
191 '%s has no annotations. Assuming "%s".', test
,
192 self
._DEFAULT
_ANNOTATION
)
193 available_tests
.append(test
)
195 available_tests
= [m
for m
in self
.GetTestMethods()
196 if not self
.IsHostDrivenTest(m
)]
198 if exclude_annotation_list
:
199 excluded_tests
= self
.GetAnnotatedTests(exclude_annotation_list
)
200 available_tests
= list(set(available_tests
) - set(excluded_tests
))
204 # |available_tests| are in adb instrument format: package.path.class#test.
206 # Maps a 'class.test' name to each 'package.path.class#test' name.
207 sanitized_test_names
= dict([
208 (t
.split('.')[-1].replace('#', '.'), t
) for t
in available_tests
])
209 # Filters 'class.test' names and populates |tests| with the corresponding
210 # 'package.path.class#test' names.
212 sanitized_test_names
[t
] for t
in unittest_util
.FilterTestNames(
213 sanitized_test_names
.keys(), test_filter
.replace('#', '.'))]
215 tests
= available_tests
217 # Filter out any tests with SDK level requirements that don't match the set
218 # of attached devices.
219 devices
= device_utils
.DeviceUtils
.parallel()
220 min_sdk_version
= min(devices
.build_version_sdk
.pGet(None))
221 tests
= [t
for t
in tests
222 if self
._IsTestValidForSdkRange
(t
, min_sdk_version
)]
227 def IsHostDrivenTest(test
):
228 return 'pythonDrivenTests' in test