1# Copyright (c) 2012 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"""Gathers information about APKs."""
6
7import collections
8import os
9import re
10
11import cmd_helper
12
13
14class ApkInfo(object):
15  """Helper class for inspecting APKs."""
16  _PROGUARD_PATH = os.path.join(os.environ['ANDROID_SDK_ROOT'],
17                                'tools/proguard/bin/proguard.sh')
18  if not os.path.exists(_PROGUARD_PATH):
19    _PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'],
20                                  'external/proguard/bin/proguard.sh')
21  _PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
22  _PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
23  _PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$')
24  _PROGUARD_ANNOTATION_CONST_RE = re.compile(r'\s*?- Constant element value.*$')
25  _PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$')
26  _AAPT_PACKAGE_NAME_RE = re.compile(r'package: .*name=\'(\S*)\'')
27
28  def __init__(self, apk_path, jar_path):
29    if not os.path.exists(apk_path):
30      raise Exception('%s not found, please build it' % apk_path)
31    self._apk_path = apk_path
32    if not os.path.exists(jar_path):
33      raise Exception('%s not found, please build it' % jar_path)
34    self._jar_path = jar_path
35    self._annotation_map = collections.defaultdict(list)
36    self._test_methods = []
37    self._Initialize()
38
39  def _Initialize(self):
40    proguard_output = cmd_helper.GetCmdOutput([self._PROGUARD_PATH,
41                                               '-injars', self._jar_path,
42                                               '-dontshrink',
43                                               '-dontoptimize',
44                                               '-dontobfuscate',
45                                               '-dontpreverify',
46                                               '-dump',
47                                              ]).split('\n')
48    clazz = None
49    method = None
50    annotation = None
51    has_value = False
52    qualified_method = None
53    for line in proguard_output:
54      m = self._PROGUARD_CLASS_RE.match(line)
55      if m:
56        clazz = m.group(1).replace('/', '.')  # Change package delim.
57        annotation = None
58        continue
59      m = self._PROGUARD_METHOD_RE.match(line)
60      if m:
61        method = m.group(1)
62        annotation = None
63        qualified_method = clazz + '#' + method
64        if method.startswith('test') and clazz.endswith('Test'):
65          self._test_methods += [qualified_method]
66        continue
67      m = self._PROGUARD_ANNOTATION_RE.match(line)
68      if m:
69        assert qualified_method
70        annotation = m.group(1).split('/')[-1]  # Ignore the annotation package.
71        self._annotation_map[qualified_method].append(annotation)
72        has_value = False
73        continue
74      if annotation:
75        assert qualified_method
76        if not has_value:
77          m = self._PROGUARD_ANNOTATION_CONST_RE.match(line)
78          if m:
79            has_value = True
80        else:
81          m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line)
82          if m:
83            value = m.group(1)
84            self._annotation_map[qualified_method].append(
85                annotation + ':' + value)
86            has_value = False
87
88  def _GetAnnotationMap(self):
89    return self._annotation_map
90
91  def _IsTestMethod(self, test):
92    class_name, method = test.split('#')
93    return class_name.endswith('Test') and method.startswith('test')
94
95  def GetApkPath(self):
96    return self._apk_path
97
98  def GetPackageName(self):
99    """Returns the package name of this APK."""
100    aapt_output = cmd_helper.GetCmdOutput(
101        ['aapt', 'dump', 'badging', self._apk_path]).split('\n')
102    for line in aapt_output:
103      m = self._AAPT_PACKAGE_NAME_RE.match(line)
104      if m:
105        return m.group(1)
106    raise Exception('Failed to determine package name of %s' % self._apk_path)
107
108  def GetTestAnnotations(self, test):
109    """Returns a list of all annotations for the given |test|. May be empty."""
110    if not self._IsTestMethod(test):
111      return []
112    return self._GetAnnotationMap()[test]
113
114  def _AnnotationsMatchFilters(self, annotation_filter_list, annotations):
115    """Checks if annotations match any of the filters."""
116    if not annotation_filter_list:
117      return True
118    for annotation_filter in annotation_filter_list:
119      filters = annotation_filter.split('=')
120      if len(filters) == 2:
121        key = filters[0]
122        value_list = filters[1].split(',')
123        for value in value_list:
124          if key + ':' + value in annotations:
125            return True
126      elif annotation_filter in annotations:
127        return True
128    return False
129
130  def GetAnnotatedTests(self, annotation_filter_list):
131    """Returns a list of all tests that match the given annotation filters."""
132    return [test for test, annotations in self._GetAnnotationMap().iteritems()
133            if self._IsTestMethod(test) and self._AnnotationsMatchFilters(
134                annotation_filter_list, annotations)]
135
136  def GetTestMethods(self):
137    """Returns a list of all test methods in this apk as Class#testMethod."""
138    return self._test_methods
139
140  @staticmethod
141  def IsPythonDrivenTest(test):
142    return 'pythonDrivenTests' in test
143