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