Index: build/android/pylib/instrumentation/test_jar.py |
diff --git a/build/android/pylib/instrumentation/test_jar.py b/build/android/pylib/instrumentation/test_jar.py |
index 183ea8639457d252512823df2374a2cd1b9ad404..c1b63c926720e6c22f1397d8a6e7425f426bc515 100644 |
--- a/build/android/pylib/instrumentation/test_jar.py |
+++ b/build/android/pylib/instrumentation/test_jar.py |
@@ -5,15 +5,16 @@ |
"""Helper class for instrumenation test jar.""" |
# pylint: disable=W0702 |
-import collections |
import logging |
import os |
import pickle |
import re |
import sys |
+import tempfile |
from pylib import cmd_helper |
from pylib import constants |
+from pylib.device import device_utils |
sys.path.insert(0, |
os.path.join(constants.DIR_SOURCE_ROOT, |
@@ -22,7 +23,7 @@ sys.path.insert(0, |
import unittest_util # pylint: disable=F0401 |
# If you change the cached output of proguard, increment this number |
-PICKLE_FORMAT_VERSION = 1 |
+PICKLE_FORMAT_VERSION = 2 |
class TestJar(object): |
@@ -31,6 +32,7 @@ class TestJar(object): |
'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest']) |
_DEFAULT_ANNOTATION = 'SmallTest' |
_PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$') |
+ _PROGUARD_SUPERCLASS_RE = re.compile(r'\s*? Superclass:\s*([\S]+)$') |
_PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$') |
_PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$') |
_PROGUARD_ANNOTATION_CONST_RE = ( |
@@ -47,9 +49,8 @@ class TestJar(object): |
self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'], |
'external/proguard/lib/proguard.jar') |
self._jar_path = jar_path |
- self._annotation_map = collections.defaultdict(list) |
self._pickled_proguard_name = self._jar_path + '-proguard.pickle' |
- self._test_methods = [] |
+ self._test_methods = {} |
if not self._GetCachedProguardData(): |
self._GetProguardData() |
@@ -63,7 +64,6 @@ class TestJar(object): |
with open(self._pickled_proguard_name, 'r') as r: |
d = pickle.loads(r.read()) |
if d['VERSION'] == PICKLE_FORMAT_VERSION: |
- self._annotation_map = d['ANNOTATION_MAP'] |
self._test_methods = d['TEST_METHODS'] |
return True |
except: |
@@ -71,68 +71,120 @@ class TestJar(object): |
return False |
def _GetProguardData(self): |
- proguard_output = cmd_helper.GetCmdOutput(['java', '-jar', |
- self._PROGUARD_PATH, |
- '-injars', self._jar_path, |
- '-dontshrink', |
- '-dontoptimize', |
- '-dontobfuscate', |
- '-dontpreverify', |
- '-dump', |
- ]).split('\n') |
- clazz = None |
- method = None |
- annotation = None |
- has_value = False |
- qualified_method = None |
- for line in proguard_output: |
- m = self._PROGUARD_CLASS_RE.match(line) |
- if m: |
- clazz = m.group(1).replace('/', '.') # Change package delim. |
- annotation = None |
- continue |
- |
- m = self._PROGUARD_METHOD_RE.match(line) |
- if m: |
- method = m.group(1) |
- annotation = None |
- qualified_method = clazz + '#' + method |
- if method.startswith('test') and clazz.endswith('Test'): |
- self._test_methods += [qualified_method] |
- continue |
- |
- if not qualified_method: |
- # Ignore non-method annotations. |
- continue |
- |
- m = self._PROGUARD_ANNOTATION_RE.match(line) |
- if m: |
- annotation = m.group(1).split('/')[-1] # Ignore the annotation package. |
- self._annotation_map[qualified_method].append(annotation) |
- has_value = False |
- continue |
- if annotation: |
- if not has_value: |
- m = self._PROGUARD_ANNOTATION_CONST_RE.match(line) |
- if m: |
- has_value = True |
- else: |
- m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line) |
+ logging.info('Retrieving test methods via proguard.') |
+ |
+ with tempfile.NamedTemporaryFile() as proguard_output: |
+ cmd_helper.RunCmd(['java', '-jar', |
+ self._PROGUARD_PATH, |
+ '-injars', self._jar_path, |
+ '-dontshrink', |
+ '-dontoptimize', |
+ '-dontobfuscate', |
+ '-dontpreverify', |
+ '-dump', proguard_output.name]) |
+ |
+ clazzez = {} |
+ |
+ annotation = None |
+ annotation_has_value = False |
+ clazz = None |
+ method = None |
+ |
+ for line in proguard_output: |
+ if len(line) == 0: |
+ annotation = None |
+ annotation_has_value = False |
+ method = None |
+ continue |
+ |
+ m = self._PROGUARD_CLASS_RE.match(line) |
+ if m: |
+ clazz = m.group(1).replace('/', '.') |
+ clazzez[clazz] = { |
+ 'methods': {}, |
+ 'annotations': {} |
+ } |
+ annotation = None |
+ annotation_has_value = False |
+ method = None |
+ continue |
+ |
+ if not clazz: |
+ continue |
+ |
+ m = self._PROGUARD_SUPERCLASS_RE.match(line) |
+ if m: |
+ clazzez[clazz]['superclass'] = m.group(1).replace('/', '.') |
+ continue |
+ |
+ if clazz.endswith('Test'): |
+ m = self._PROGUARD_METHOD_RE.match(line) |
if m: |
- value = m.group(1) |
- self._annotation_map[qualified_method].append( |
- annotation + ':' + value) |
- has_value = False |
+ method = m.group(1) |
+ clazzez[clazz]['methods'][method] = {'annotations': {}} |
+ annotation = None |
+ annotation_has_value = False |
+ continue |
+ |
+ m = self._PROGUARD_ANNOTATION_RE.match(line) |
+ if m: |
+ # Ignore the annotation package. |
+ annotation = m.group(1).split('/')[-1] |
+ if method: |
+ clazzez[clazz]['methods'][method]['annotations'][annotation] = None |
+ else: |
+ clazzez[clazz]['annotations'][annotation] = None |
+ continue |
+ |
+ if annotation: |
+ if not annotation_has_value: |
+ m = self._PROGUARD_ANNOTATION_CONST_RE.match(line) |
+ annotation_has_value = bool(m) |
+ else: |
+ m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line) |
+ if m: |
+ if method: |
+ clazzez[clazz]['methods'][method]['annotations'][annotation] = ( |
+ m.group(1)) |
+ else: |
+ clazzez[clazz]['annotations'][annotation] = m.group(1) |
+ annotation_has_value = None |
+ |
+ test_clazzez = ((n, i) for n, i in clazzez.items() if n.endswith('Test')) |
+ for clazz_name, clazz_info in test_clazzez: |
+ logging.info('Processing %s' % clazz_name) |
+ c = clazz_name |
+ min_sdk_level = None |
+ |
+ while c in clazzez: |
+ c_info = clazzez[c] |
+ if not min_sdk_level: |
+ min_sdk_level = c_info['annotations'].get('MinAndroidSdkLevel', None) |
+ c = c_info.get('superclass', None) |
+ |
+ for method_name, method_info in clazz_info['methods'].items(): |
+ if method_name.startswith('test'): |
+ qualified_method = '%s#%s' % (clazz_name, method_name) |
+ method_annotations = [] |
+ for annotation_name, annotation_value in ( |
+ method_info['annotations'].items()): |
+ method_annotations.append(annotation_name) |
+ if annotation_value: |
+ method_annotations.append( |
+ annotation_name + ':' + annotation_value) |
+ self._test_methods[qualified_method] = { |
+ 'annotations': method_annotations |
+ } |
+ if min_sdk_level is not None: |
+ self._test_methods[qualified_method]['min_sdk_level'] = ( |
+ int(min_sdk_level)) |
logging.info('Storing proguard output to %s', self._pickled_proguard_name) |
d = {'VERSION': PICKLE_FORMAT_VERSION, |
- 'ANNOTATION_MAP': self._annotation_map, |
'TEST_METHODS': self._test_methods} |
with open(self._pickled_proguard_name, 'w') as f: |
f.write(pickle.dumps(d)) |
- def _GetAnnotationMap(self): |
- return self._annotation_map |
@staticmethod |
def _IsTestMethod(test): |
@@ -141,9 +193,9 @@ class TestJar(object): |
def GetTestAnnotations(self, test): |
"""Returns a list of all annotations for the given |test|. May be empty.""" |
- if not self._IsTestMethod(test): |
+ if not self._IsTestMethod(test) or not test in self._test_methods: |
return [] |
- return self._GetAnnotationMap()[test] |
+ return self._test_methods[test]['annotations'] |
@staticmethod |
def _AnnotationsMatchFilters(annotation_filter_list, annotations): |
@@ -164,24 +216,33 @@ class TestJar(object): |
def GetAnnotatedTests(self, annotation_filter_list): |
"""Returns a list of all tests that match the given annotation filters.""" |
- return [test for test, annotations in self._GetAnnotationMap().iteritems() |
+ return [test for test, attr in self.GetTestMethods().iteritems() |
if self._IsTestMethod(test) and self._AnnotationsMatchFilters( |
- annotation_filter_list, annotations)] |
+ annotation_filter_list, attr['annotations'])] |
def GetTestMethods(self): |
- """Returns a list of all test methods in this apk as Class#testMethod.""" |
+ """Returns a dict of all test methods and relevant attributes. |
+ |
+ Test methods are retrieved as Class#testMethod. |
+ """ |
return self._test_methods |
def _GetTestsMissingAnnotation(self): |
"""Get a list of test methods with no known annotations.""" |
tests_missing_annotations = [] |
- for test_method in self.GetTestMethods(): |
+ for test_method in self.GetTestMethods().iterkeys(): |
annotations_ = frozenset(self.GetTestAnnotations(test_method)) |
if (annotations_.isdisjoint(self._ANNOTATIONS) and |
not self.IsHostDrivenTest(test_method)): |
tests_missing_annotations.append(test_method) |
return sorted(tests_missing_annotations) |
+ def _IsTestValidForSdkRange(self, test_name, attached_min_sdk_level): |
+ required_min_sdk_level = self.GetTestMethods()[test_name].get( |
+ 'min_sdk_level', None) |
+ return (required_min_sdk_level is None or |
+ attached_min_sdk_level >= required_min_sdk_level) |
+ |
def GetAllMatchingTests(self, annotation_filter_list, |
exclude_annotation_list, test_filter): |
"""Get a list of tests matching any of the annotations and the filter. |
@@ -228,6 +289,16 @@ class TestJar(object): |
else: |
tests = available_tests |
+ # Filter out any tests with SDK level requirements that don't match the set |
+ # of attached devices. |
+ sdk_versions = [ |
+ int(v) for v in |
+ device_utils.DeviceUtils.parallel().GetProp( |
+ 'ro.build.version.sdk').pGet(None)] |
+ tests = filter( |
+ lambda t: self._IsTestValidForSdkRange(t, min(sdk_versions)), |
+ tests) |
+ |
return tests |
@staticmethod |