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"""Base class for Android Python-driven tests.
6
7This test case is intended to serve as the base class for any Python-driven
8tests. It is similar to the Python unitttest module in that the user's tests
9inherit from this case and add their tests in that case.
10
11When a PythonTestBase object is instantiated, its purpose is to run only one of
12its tests. The test runner gives it the name of the test the instance will
13run. The test runner calls SetUp with the Android device ID which the test will
14run against. The runner runs the test method itself, collecting the result,
15and calls TearDown.
16
17Tests can basically do whatever they want in the test methods, such as call
18Java tests using _RunJavaTests. Those methods have the advantage of massaging
19the Java test results into Python test results.
20"""
21
22import logging
23import os
24import time
25
26import android_commands
27import apk_info
28from run_java_tests import TestRunner
29from test_result import SingleTestResult, TestResults
30
31
32# aka the parent of com.google.android
33BASE_ROOT = 'src' + os.sep
34
35
36class PythonTestBase(object):
37  """Base class for Python-driven tests."""
38
39  def __init__(self, test_name):
40    # test_name must match one of the test methods defined on a subclass which
41    # inherits from this class.
42    # It's stored so we can do the attr lookup on demand, allowing this class
43    # to be pickled, a requirement for the multiprocessing module.
44    self.test_name = test_name
45    class_name = self.__class__.__name__
46    self.qualified_name = class_name + '.' + self.test_name
47
48  def SetUp(self, options):
49    self.options = options
50    self.shard_index = self.options.shard_index
51    self.device_id = self.options.device_id
52    self.adb = android_commands.AndroidCommands(self.device_id)
53    self.ports_to_forward = []
54
55  def TearDown(self):
56    pass
57
58  def Run(self):
59    logging.warning('Running Python-driven test: %s', self.test_name)
60    return getattr(self, self.test_name)()
61
62  def _RunJavaTest(self, fname, suite, test):
63    """Runs a single Java test with a Java TestRunner.
64
65    Args:
66      fname: filename for the test (e.g. foo/bar/baz/tests/FooTest.py)
67      suite: name of the Java test suite (e.g. FooTest)
68      test: name of the test method to run (e.g. testFooBar)
69
70    Returns:
71      TestResults object with a single test result.
72    """
73    test = self._ComposeFullTestName(fname, suite, test)
74    apks = [apk_info.ApkInfo(self.options.test_apk_path,
75            self.options.test_apk_jar_path)]
76    java_test_runner = TestRunner(self.options, self.device_id, [test], False,
77                                  self.shard_index,
78                                  apks,
79                                  self.ports_to_forward)
80    return java_test_runner.Run()
81
82  def _RunJavaTests(self, fname, tests):
83    """Calls a list of tests and stops at the first test failure.
84
85    This method iterates until either it encounters a non-passing test or it
86    exhausts the list of tests. Then it returns the appropriate Python result.
87
88    Args:
89      fname: filename for the Python test
90      tests: a list of Java test names which will be run
91
92    Returns:
93      A TestResults object containing a result for this Python test.
94    """
95    start_ms = int(time.time()) * 1000
96
97    result = None
98    for test in tests:
99      # We're only running one test at a time, so this TestResults object will
100      # hold only one result.
101      suite, test_name = test.split('.')
102      result = self._RunJavaTest(fname, suite, test_name)
103      # A non-empty list means the test did not pass.
104      if result.GetAllBroken():
105        break
106
107    duration_ms = int(time.time()) * 1000 - start_ms
108
109    # Do something with result.
110    return self._ProcessResults(result, start_ms, duration_ms)
111
112  def _ProcessResults(self, result, start_ms, duration_ms):
113    """Translates a Java test result into a Python result for this test.
114
115    The TestRunner class that we use under the covers will return a test result
116    for that specific Java test. However, to make reporting clearer, we have
117    this method to abstract that detail and instead report that as a failure of
118    this particular test case while still including the Java stack trace.
119
120    Args:
121      result: TestResults with a single Java test result
122      start_ms: the time the test started
123      duration_ms: the length of the test
124
125    Returns:
126      A TestResults object containing a result for this Python test.
127    """
128    test_results = TestResults()
129
130    # If our test is in broken, then it crashed/failed.
131    broken = result.GetAllBroken()
132    if broken:
133      # Since we have run only one test, take the first and only item.
134      single_result = broken[0]
135
136      log = single_result.log
137      if not log:
138        log = 'No logging information.'
139
140      python_result = SingleTestResult(self.qualified_name, start_ms,
141                                       duration_ms,
142                                       log)
143
144      # Figure out where the test belonged. There's probably a cleaner way of
145      # doing this.
146      if single_result in result.crashed:
147        test_results.crashed = [python_result]
148      elif single_result in result.failed:
149        test_results.failed = [python_result]
150      elif single_result in result.unknown:
151        test_results.unknown = [python_result]
152
153    else:
154      python_result = SingleTestResult(self.qualified_name, start_ms,
155                                       duration_ms)
156      test_results.ok = [python_result]
157
158    return test_results
159
160  def _ComposeFullTestName(self, fname, suite, test):
161    package_name = self._GetPackageName(fname)
162    return package_name + '.' + suite + '#' + test
163
164  def _GetPackageName(self, fname):
165    """Extracts the package name from the test file path."""
166    dirname = os.path.dirname(fname)
167    package = dirname[dirname.rfind(BASE_ROOT) + len(BASE_ROOT):]
168    return package.replace(os.sep, '.')
169