1#!/usr/bin/python2.4
2#
3#
4# Copyright 2008, The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18"""TestSuite definition for Android instrumentation tests."""
19
20import os
21import re
22
23# local imports
24import android_manifest
25from coverage import coverage
26import errors
27import logger
28import test_suite
29
30
31class InstrumentationTestSuite(test_suite.AbstractTestSuite):
32  """Represents a java instrumentation test suite definition run on device."""
33
34  DEFAULT_RUNNER = "android.test.InstrumentationTestRunner"
35
36  def __init__(self):
37    test_suite.AbstractTestSuite.__init__(self)
38    self._package_name = None
39    self._runner_name = self.DEFAULT_RUNNER
40    self._class_name = None
41    self._target_name = None
42    self._java_package = None
43
44  def GetPackageName(self):
45    return self._package_name
46
47  def SetPackageName(self, package_name):
48    self._package_name = package_name
49    return self
50
51  def GetRunnerName(self):
52    return self._runner_name
53
54  def SetRunnerName(self, runner_name):
55    self._runner_name = runner_name
56    return self
57
58  def GetClassName(self):
59    return self._class_name
60
61  def SetClassName(self, class_name):
62    self._class_name = class_name
63    return self
64
65  def GetJavaPackageFilter(self):
66    return self._java_package
67
68  def SetJavaPackageFilter(self, java_package_name):
69    """Configure the suite to only run tests in given java package."""
70    self._java_package = java_package_name
71    return self
72
73  def GetTargetName(self):
74    """Retrieve module that this test is targeting.
75
76    Used for generating code coverage metrics.
77    Returns:
78      the module target name
79    """
80    return self._target_name
81
82  def SetTargetName(self, target_name):
83    self._target_name = target_name
84    return self
85
86  def GetBuildDependencies(self, options):
87    if options.coverage_target_path:
88      return [options.coverage_target_path]
89    return []
90
91  def Run(self, options, adb):
92    """Run the provided test suite.
93
94    Builds up an adb instrument command using provided input arguments.
95
96    Args:
97      options: command line options to provide to test run
98      adb: adb_interface to device under test
99
100    Raises:
101      errors.AbortError: if fatal error occurs
102    """
103
104    test_class = self.GetClassName()
105    if options.test_class is not None:
106      test_class = options.test_class.lstrip()
107      if test_class.startswith("."):
108        test_class = self.GetPackageName() + test_class
109    if options.test_method is not None:
110      test_class = "%s#%s" % (test_class, options.test_method)
111
112    test_package = self.GetJavaPackageFilter()
113    if options.test_package:
114      test_package = options.test_package
115
116    if test_class and test_package:
117      logger.Log('Error: both class and java package options are specified')
118
119    instrumentation_args = {}
120    if test_class is not None:
121      instrumentation_args["class"] = test_class
122    if test_package:
123      instrumentation_args["package"] = test_package
124    if options.test_size:
125      instrumentation_args["size"] = options.test_size
126    if options.wait_for_debugger:
127      instrumentation_args["debug"] = "true"
128    if options.suite_assign_mode:
129      instrumentation_args["suiteAssignment"] = "true"
130    if options.coverage:
131      instrumentation_args["coverage"] = "true"
132    if options.test_annotation:
133      instrumentation_args["annotation"] = options.test_annotation
134    if options.test_not_annotation:
135      instrumentation_args["notAnnotation"] = options.test_not_annotation
136    if options.preview:
137      adb_cmd = adb.PreviewInstrumentationCommand(
138          package_name=self.GetPackageName(),
139          runner_name=self.GetRunnerName(),
140          raw_mode=options.raw_mode,
141          instrumentation_args=instrumentation_args)
142      logger.Log(adb_cmd)
143    elif options.coverage:
144      coverage_gen = coverage.CoverageGenerator(adb)
145      if options.coverage_target_path:
146        coverage_target = coverage_gen.GetCoverageTargetForPath(options.coverage_target_path)
147      elif self.GetTargetName():
148        coverage_target = coverage_gen.GetCoverageTarget(self.GetTargetName())
149      self._CheckInstrumentationInstalled(adb)
150      # need to parse test output to determine path to coverage file
151      logger.Log("Running in coverage mode, suppressing test output")
152      try:
153        (test_results, status_map) = adb.StartInstrumentationForPackage(
154            package_name=self.GetPackageName(),
155            runner_name=self.GetRunnerName(),
156            timeout_time=60*60,
157            instrumentation_args=instrumentation_args,
158            user=options.user,
159            no_hidden_api_checks=options.no_hidden_api_checks)
160      except errors.InstrumentationError, errors.DeviceUnresponsiveError:
161        return
162      self._PrintTestResults(test_results)
163      device_coverage_path = status_map.get("coverageFilePath", None)
164      if device_coverage_path is None:
165        logger.Log("Error: could not find coverage data on device")
166        return
167
168      coverage_file = coverage_gen.ExtractReport(
169          self.GetName(), coverage_target, device_coverage_path,
170          test_qualifier=options.test_size)
171      if coverage_file is not None:
172        logger.Log("Coverage report generated at %s" % coverage_file)
173
174    else:
175      self._CheckInstrumentationInstalled(adb)
176      adb.StartInstrumentationNoResults(
177          package_name=self.GetPackageName(),
178          runner_name=self.GetRunnerName(),
179          raw_mode=options.raw_mode,
180          instrumentation_args=instrumentation_args,
181          user=options.user,
182          no_hidden_api_checks=options.no_hidden_api_checks)
183
184  def _CheckInstrumentationInstalled(self, adb):
185    if not adb.IsInstrumentationInstalled(self.GetPackageName(),
186                                          self.GetRunnerName()):
187      msg=("Could not find instrumentation %s/%s on device. Try forcing a "
188           "rebuild by updating a source file, and re-executing runtest." %
189           (self.GetPackageName(), self.GetRunnerName()))
190      raise errors.AbortError(msg=msg)
191
192  def _PrintTestResults(self, test_results):
193    """Prints a summary of test result data to stdout.
194
195    Args:
196      test_results: a list of am_instrument_parser.TestResult
197    """
198    total_count = 0
199    error_count = 0
200    fail_count = 0
201    for test_result in test_results:
202      if test_result.GetStatusCode() == -1:  # error
203        logger.Log("Error in %s: %s" % (test_result.GetTestName(),
204                                        test_result.GetFailureReason()))
205        error_count+=1
206      elif test_result.GetStatusCode() == -2:  # failure
207        logger.Log("Failure in %s: %s" % (test_result.GetTestName(),
208                                          test_result.GetFailureReason()))
209        fail_count+=1
210      total_count+=1
211    logger.Log("Tests run: %d, Failures: %d, Errors: %d" %
212               (total_count, fail_count, error_count))
213
214def HasInstrumentationTest(path):
215  """Determine if given path defines an instrumentation test.
216
217  Args:
218    path: file system path to instrumentation test.
219  """
220  manifest_parser = android_manifest.CreateAndroidManifest(path)
221  if manifest_parser:
222    return manifest_parser.GetInstrumentationNames()
223  return False
224
225class InstrumentationTestFactory(test_suite.AbstractTestFactory):
226  """A factory for creating InstrumentationTestSuites"""
227
228  def __init__(self, test_root_path, build_path):
229    test_suite.AbstractTestFactory.__init__(self, test_root_path,
230                                            build_path)
231
232  def CreateTests(self, sub_tests_path=None):
233    """Create tests found in test_path.
234
235    Will create a single InstrumentationTestSuite based on info found in
236    AndroidManifest.xml found at build_path. Will set additional filters if
237    test_path refers to a java package or java class.
238    """
239    tests = []
240    class_name_arg = None
241    java_package_name = None
242    if sub_tests_path:
243      # if path is java file, populate class name
244      if self._IsJavaFile(sub_tests_path):
245        class_name_arg = self._GetClassNameFromFile(sub_tests_path)
246        logger.SilentLog('Using java test class %s' % class_name_arg)
247      elif self._IsJavaPackage(sub_tests_path):
248        java_package_name = self._GetPackageNameFromDir(sub_tests_path)
249        logger.SilentLog('Using java package %s' % java_package_name)
250    try:
251      manifest_parser = android_manifest.AndroidManifest(app_path=
252                                                         self.GetTestsRootPath())
253      instrs = manifest_parser.GetInstrumentationNames()
254      if not instrs:
255        logger.Log('Could not find instrumentation declarations in %s at %s' %
256                   (android_manifest.AndroidManifest.FILENAME,
257                    self.GetBuildPath()))
258        return tests
259      elif len(instrs) > 1:
260        logger.Log("Found multiple instrumentation declarations in %s/%s. "
261                   "Only using first declared." %
262                   (self.GetBuildPath(),
263                    android_manifest.AndroidManifest.FILENAME))
264      instr_name = manifest_parser.GetInstrumentationNames()[0]
265      # escape inner class names
266      instr_name = instr_name.replace('$', '\$')
267      pkg_name = manifest_parser.GetPackageName()
268      if instr_name.find(".") < 0:
269        instr_name = "." + instr_name
270      logger.SilentLog('Found instrumentation %s/%s' % (pkg_name, instr_name))
271      suite = InstrumentationTestSuite()
272      suite.SetPackageName(pkg_name)
273      suite.SetBuildPath(self.GetBuildPath())
274      suite.SetRunnerName(instr_name)
275      suite.SetName(pkg_name)
276      suite.SetClassName(class_name_arg)
277      suite.SetJavaPackageFilter(java_package_name)
278      tests.append(suite)
279      return tests
280
281    except:
282      logger.Log('Could not find or parse %s at %s' %
283                 (android_manifest.AndroidManifest.FILENAME,
284                  self.GetBuildPath()))
285    return tests
286
287  def _IsJavaFile(self, path):
288    """Returns true if given file system path is a java file."""
289    return os.path.isfile(path) and self._IsJavaFileName(path)
290
291  def _IsJavaFileName(self, filename):
292    """Returns true if given file name is a java file name."""
293    return os.path.splitext(filename)[1] == '.java'
294
295  def _IsJavaPackage(self, path):
296    """Returns true if given file path is a java package.
297
298    Currently assumes if any java file exists in this directory, than it
299    represents a java package.
300
301    Args:
302      path: file system path of directory to check
303
304    Returns:
305      True if path is a java package
306    """
307    if not os.path.isdir(path):
308      return False
309    for file_name in os.listdir(path):
310      if self._IsJavaFileName(file_name):
311        return True
312    return False
313
314  def _GetClassNameFromFile(self, java_file_path):
315    """Gets the fully qualified java class name from path.
316
317    Args:
318      java_file_path: file system path of java file
319
320    Returns:
321      fully qualified java class name or None.
322    """
323    package_name = self._GetPackageNameFromFile(java_file_path)
324    if package_name:
325      filename = os.path.basename(java_file_path)
326      class_name = os.path.splitext(filename)[0]
327      return '%s.%s' % (package_name, class_name)
328    return None
329
330  def _GetPackageNameFromDir(self, path):
331    """Gets the java package name associated with given directory path.
332
333    Caveat: currently just parses defined java package name from first java
334    file found in directory.
335
336    Args:
337      path: file system path of directory
338
339    Returns:
340      the java package name or None
341    """
342    for filename in os.listdir(path):
343      if self._IsJavaFileName(filename):
344        return self._GetPackageNameFromFile(os.path.join(path, filename))
345
346  def _GetPackageNameFromFile(self, java_file_path):
347    """Gets the java package name associated with given java file path.
348
349    Args:
350      java_file_path: file system path of java file
351
352    Returns:
353      the java package name or None
354    """
355    logger.SilentLog('Looking for java package name in %s' % java_file_path)
356    re_package = re.compile(r'package\s+(.*);')
357    file_handle = open(java_file_path, 'r')
358    for line in file_handle:
359      match = re_package.match(line)
360      if match:
361        return match.group(1)
362    return None
363