• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5"""Helper class for instrumenation test jar."""
6# pylint: disable=W0702
7
8import collections
9import logging
10import os
11import pickle
12import re
13import sys
14
15from pylib import cmd_helper
16from pylib import constants
17
18sys.path.insert(0,
19                os.path.join(constants.DIR_SOURCE_ROOT,
20                             'build', 'util', 'lib', 'common'))
21
22import unittest_util # pylint: disable=F0401
23
24# If you change the cached output of proguard, increment this number
25PICKLE_FORMAT_VERSION = 1
26
27
28class TestJar(object):
29  _ANNOTATIONS = frozenset(
30      ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest',
31       'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest', 'HostDrivenTest'])
32  _DEFAULT_ANNOTATION = 'SmallTest'
33  _PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
34  _PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
35  _PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$')
36  _PROGUARD_ANNOTATION_CONST_RE = (
37      re.compile(r'\s*?- Constant element value.*$'))
38  _PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$')
39
40  def __init__(self, jar_path):
41    if not os.path.exists(jar_path):
42      raise Exception('%s not found, please build it' % jar_path)
43
44    self._PROGUARD_PATH = os.path.join(constants.ANDROID_SDK_ROOT,
45                                       'tools/proguard/lib/proguard.jar')
46    if not os.path.exists(self._PROGUARD_PATH):
47      self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'],
48                                         'external/proguard/lib/proguard.jar')
49    self._jar_path = jar_path
50    self._annotation_map = collections.defaultdict(list)
51    self._pickled_proguard_name = self._jar_path + '-proguard.pickle'
52    self._test_methods = []
53    if not self._GetCachedProguardData():
54      self._GetProguardData()
55
56  def _GetCachedProguardData(self):
57    if (os.path.exists(self._pickled_proguard_name) and
58        (os.path.getmtime(self._pickled_proguard_name) >
59         os.path.getmtime(self._jar_path))):
60      logging.info('Loading cached proguard output from %s',
61                   self._pickled_proguard_name)
62      try:
63        with open(self._pickled_proguard_name, 'r') as r:
64          d = pickle.loads(r.read())
65        if d['VERSION'] == PICKLE_FORMAT_VERSION:
66          self._annotation_map = d['ANNOTATION_MAP']
67          self._test_methods = d['TEST_METHODS']
68          return True
69      except:
70        logging.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache')
71    return False
72
73  def _GetProguardData(self):
74    proguard_output = cmd_helper.GetCmdOutput(['java', '-jar',
75                                               self._PROGUARD_PATH,
76                                               '-injars', self._jar_path,
77                                               '-dontshrink',
78                                               '-dontoptimize',
79                                               '-dontobfuscate',
80                                               '-dontpreverify',
81                                               '-dump',
82                                              ]).split('\n')
83    clazz = None
84    method = None
85    annotation = None
86    has_value = False
87    qualified_method = None
88    for line in proguard_output:
89      m = self._PROGUARD_CLASS_RE.match(line)
90      if m:
91        clazz = m.group(1).replace('/', '.')  # Change package delim.
92        annotation = None
93        continue
94
95      m = self._PROGUARD_METHOD_RE.match(line)
96      if m:
97        method = m.group(1)
98        annotation = None
99        qualified_method = clazz + '#' + method
100        if method.startswith('test') and clazz.endswith('Test'):
101          self._test_methods += [qualified_method]
102        continue
103
104      if not qualified_method:
105        # Ignore non-method annotations.
106        continue
107
108      m = self._PROGUARD_ANNOTATION_RE.match(line)
109      if m:
110        annotation = m.group(1).split('/')[-1]  # Ignore the annotation package.
111        self._annotation_map[qualified_method].append(annotation)
112        has_value = False
113        continue
114      if annotation:
115        if not has_value:
116          m = self._PROGUARD_ANNOTATION_CONST_RE.match(line)
117          if m:
118            has_value = True
119        else:
120          m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line)
121          if m:
122            value = m.group(1)
123            self._annotation_map[qualified_method].append(
124                annotation + ':' + value)
125            has_value = False
126
127    logging.info('Storing proguard output to %s', self._pickled_proguard_name)
128    d = {'VERSION': PICKLE_FORMAT_VERSION,
129         'ANNOTATION_MAP': self._annotation_map,
130         'TEST_METHODS': self._test_methods}
131    with open(self._pickled_proguard_name, 'w') as f:
132      f.write(pickle.dumps(d))
133
134  def _GetAnnotationMap(self):
135    return self._annotation_map
136
137  @staticmethod
138  def _IsTestMethod(test):
139    class_name, method = test.split('#')
140    return class_name.endswith('Test') and method.startswith('test')
141
142  def GetTestAnnotations(self, test):
143    """Returns a list of all annotations for the given |test|. May be empty."""
144    if not self._IsTestMethod(test):
145      return []
146    return self._GetAnnotationMap()[test]
147
148  @staticmethod
149  def _AnnotationsMatchFilters(annotation_filter_list, annotations):
150    """Checks if annotations match any of the filters."""
151    if not annotation_filter_list:
152      return True
153    for annotation_filter in annotation_filter_list:
154      filters = annotation_filter.split('=')
155      if len(filters) == 2:
156        key = filters[0]
157        value_list = filters[1].split(',')
158        for value in value_list:
159          if key + ':' + value in annotations:
160            return True
161      elif annotation_filter in annotations:
162        return True
163    return False
164
165  def GetAnnotatedTests(self, annotation_filter_list):
166    """Returns a list of all tests that match the given annotation filters."""
167    return [test for test, annotations in self._GetAnnotationMap().iteritems()
168            if self._IsTestMethod(test) and self._AnnotationsMatchFilters(
169                annotation_filter_list, annotations)]
170
171  def GetTestMethods(self):
172    """Returns a list of all test methods in this apk as Class#testMethod."""
173    return self._test_methods
174
175  def _GetTestsMissingAnnotation(self):
176    """Get a list of test methods with no known annotations."""
177    tests_missing_annotations = []
178    for test_method in self.GetTestMethods():
179      annotations_ = frozenset(self.GetTestAnnotations(test_method))
180      if (annotations_.isdisjoint(self._ANNOTATIONS) and
181          not self.IsHostDrivenTest(test_method)):
182        tests_missing_annotations.append(test_method)
183    return sorted(tests_missing_annotations)
184
185  def GetAllMatchingTests(self, annotation_filter_list,
186                          exclude_annotation_list, test_filter):
187    """Get a list of tests matching any of the annotations and the filter.
188
189    Args:
190      annotation_filter_list: List of test annotations. A test must have at
191        least one of these annotations. A test without any annotations is
192        considered to be SmallTest.
193      exclude_annotation_list: List of test annotations. A test must not have
194        any of these annotations.
195      test_filter: Filter used for partial matching on the test method names.
196
197    Returns:
198      List of all matching tests.
199    """
200    if annotation_filter_list:
201      available_tests = self.GetAnnotatedTests(annotation_filter_list)
202      # Include un-annotated tests in SmallTest.
203      if annotation_filter_list.count(self._DEFAULT_ANNOTATION) > 0:
204        for test in self._GetTestsMissingAnnotation():
205          logging.warning(
206              '%s has no annotations. Assuming "%s".', test,
207              self._DEFAULT_ANNOTATION)
208          available_tests.append(test)
209      if exclude_annotation_list:
210        excluded_tests = self.GetAnnotatedTests(exclude_annotation_list)
211        available_tests = list(set(available_tests) - set(excluded_tests))
212    else:
213      available_tests = [m for m in self.GetTestMethods()
214                         if not self.IsHostDrivenTest(m)]
215
216    tests = []
217    if test_filter:
218      # |available_tests| are in adb instrument format: package.path.class#test.
219
220      # Maps a 'class.test' name to each 'package.path.class#test' name.
221      sanitized_test_names = dict([
222          (t.split('.')[-1].replace('#', '.'), t) for t in available_tests])
223      # Filters 'class.test' names and populates |tests| with the corresponding
224      # 'package.path.class#test' names.
225      tests = [
226          sanitized_test_names[t] for t in unittest_util.FilterTestNames(
227              sanitized_test_names.keys(), test_filter.replace('#', '.'))]
228    else:
229      tests = available_tests
230
231    return tests
232
233  @staticmethod
234  def IsHostDrivenTest(test):
235    return 'pythonDrivenTests' in test
236