1# Copyright (C) 2016 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15'''Module that contains TestBase, the base class of all tests.''' 16 17from __future__ import absolute_import 18 19import logging 20import os 21import re 22import tempfile 23import inspect 24import traceback 25 26from .exception import DisconnectedException, TestSuiteException 27 28from . import util_log 29 30 31class TestBase(object): 32 '''Base class for all tests. Provides some common functionality.''' 33 34 bundle_target = {} 35 36 class TestFail(Exception): 37 '''Exception that is thrown when a line in a test fails. 38 39 This exception is thrown if a lldb command does not return the expected 40 string. 41 ''' 42 pass 43 44 def __init__(self, device_port, device, timer, app_type, wimpy=False, **kwargs): 45 # Keep argument names for documentation purposes. This method is 46 # overwritten by test_base_remote. 47 # pylint: disable=unused-argument 48 self._lldb = None # handle to the lldb module 49 self._ci = None # instance of the lldb command interpreter for this test 50 self._timer = timer # timer instance, to check whether the test froze 51 self.app_type = app_type # The type of bundle that is being executed 52 self.wimpy = wimpy 53 54 def setup(self, android): 55 '''Set up environment for the test. 56 57 Override to specify commands to be run before the test APK launch. 58 Useful for setting Android properties or environment variables. See also 59 the teardown method. 60 61 Args: 62 android: Handler to the android device, see the UtilAndroid class. 63 ''' 64 pass 65 66 def teardown(self, android): 67 '''Clean up environment after test. 68 69 Override this procedure to specify commands to be run after the test has 70 finished. This method is run regardless the outcome of the test. 71 72 Args: 73 android: Handler to the android device, see the UtilAndroid class. 74 ''' 75 pass 76 77 def run(self, dbg, remote_pid, lldb): 78 '''Execute the actual test suite. 79 80 Args: 81 dbg: The instance of the SBDebugger that is used to test commands. 82 remote_pid: The integer that is the process id of the binary that 83 the debugger is attached to. 84 lldb: A handle to the lldb module. 85 86 Returns: 87 A list of (test, failure) tuples. 88 ''' 89 log = util_log.get_logger() 90 91 def predicate(obj): 92 '''check whether we're interested in the function''' 93 if not callable(obj): 94 return False 95 if self.wimpy and not getattr(obj, 'wimpy', False): 96 log.debug("skipping non-wimpy test in wimpy mode:%r", obj) 97 return False 98 return True 99 100 test_methods = [ 101 method for name, method in inspect.getmembers(self, predicate) 102 if name.startswith('test_') 103 ] 104 log.debug("Found the following tests %r", test_methods) 105 test_errors = [] 106 107 for test in sorted( 108 test_methods, 109 key=lambda item: getattr(item, 'test_order', float('Inf')) 110 ): 111 try: 112 log.info("running test %r", test.__name__) 113 result = test() 114 except (self.TestFail, TestSuiteException) as e: 115 test_errors.append((method, e)) 116 117 return test_errors 118 119 def post_run(self): 120 '''Clean up after test execution.''' 121 pass 122 123 def assert_true(self, cond): 124 '''Check a given condition and raise TestFail if it is False. 125 126 Args: 127 cond: The boolean condition to check. 128 129 Raises: 130 TestFail: The condition was false. 131 ''' 132 if not cond: 133 raise self.TestFail() 134 135 def assert_lang_renderscript(self): 136 '''Check that LLDB is stopped in a RenderScript frame 137 138 Use the LLDB API to check that the language of the current frame 139 is RenderScript, fail otherwise. 140 141 Raises: 142 TestFail: Detected language not RenderScript. 143 ''' 144 assert self._lldb 145 assert self._ci 146 147 proc = self._ci.GetProcess() 148 frame = proc.GetSelectedThread().GetSelectedFrame() 149 lang = frame.GetCompileUnit().GetLanguage() 150 151 if lang != self._lldb.eLanguageTypeExtRenderScript: 152 raise self.TestFail('Frame language not RenderScript, instead {0}' 153 .format(lang)) 154 155 def do_command(self, cmd): 156 '''Run an lldb command and return the output. 157 158 Args: 159 cmd: The string representing the lldb command to run. 160 161 Raises: 162 TestFail: The lldb command failed. 163 ''' 164 assert self._lldb 165 assert self._ci 166 167 log = util_log.get_logger() 168 res = self._lldb.SBCommandReturnObject() 169 170 log.info('[Command] {0}'.format(cmd)) 171 172 # before issuing the command, restart the current timer to check 173 # whether the command is going to freeze the test 174 if self._timer: 175 self._timer.reset() 176 177 self._ci.HandleCommand(cmd, res) 178 179 if not res.Succeeded(): 180 error = res.GetError() 181 error = error if error else res.GetOutput() 182 raise self.TestFail('The command "{0}" failed with the error: {1}' 183 .format(cmd, error if error else '<N/a>')) 184 185 output = res.GetOutput() or '' 186 log.debug('[Output] {0}'.format(output.rstrip())) 187 188 return output 189 190 def try_command(self, cmd, expected=None, expected_regex=None): 191 '''Run an lldb command and match the expected response. 192 193 Args: 194 cmd: The string representing the lldb command to run. 195 expected: A list of strings that should be present in lldb's 196 output. 197 expected_regex: A list of regular expressions that should 198 match lldb's output. 199 200 Raises: 201 TestFail: One of the expected strings were not found in the lldb 202 output. 203 204 Returns: 205 str: raw lldb command output. 206 ''' 207 assert self._lldb 208 assert self._ci 209 log = util_log.get_logger() 210 output = '' 211 try: 212 output = self.do_command(cmd) 213 214 if 'lost connection' in output: 215 raise DisconnectedException('Lost connection to lldb-server.') 216 217 # check the expected strings 218 if expected: 219 self._match_literals(output, expected) 220 221 # check the regexp patterns 222 if expected_regex: 223 self._match_regexp_patterns(output, expected_regex) 224 225 except self.TestFail as exception: 226 # if the command failed, ensure the output retrieved from the 227 # command is printed even in verbose mode 228 if log.getEffectiveLevel() > logging.DEBUG: 229 log.error('[Output] {0}'.format(output.rstrip() if output 230 else '<empty>')) 231 232 # print the back trace, it should help to identify the error in 233 # the test 234 backtrace = ['[Back trace]'] 235 for (filename, line, function, text) in \ 236 traceback.extract_stack()[:-1]: 237 backtrace.append(' [{0} line: {2} fn: {1}] {3}'.format( 238 filename, function, line, text 239 ) 240 ) 241 log.error('\n'.join(backtrace)) 242 log.error('[TEST ERROR] {0}'.format(exception.message)) 243 raise # pass through 244 245 return output 246 247 def _match_literals(self, text, literals): 248 '''Checks the text against the array of literals. 249 250 Raises a TestFail exception in case one of the literals is not contained 251 in the text. 252 253 Args: 254 text: String, it represents the text to match. 255 literals: an array of string literals to match in the output. 256 257 Throws: self.TestFail: if it cannot match one of the literals in 258 the output. 259 ''' 260 for string in literals: 261 if string not in text: 262 raise self.TestFail('Cannot find "{0}" in the output' 263 .format(string)) 264 265 def _match_regexp_patterns(self, text, patterns): 266 '''Checks the text against the array of regular expression patterns. 267 268 Raises a TestFail exception in case one of the patterns is not matched 269 in the given text. 270 271 Args: 272 text: String, it represents the text to match. 273 patterns: an array of strings, each of them representing a regular 274 expression to match in text. 275 276 Throws: self.TestFail: if it cannot match one of the literals in 277 the output. 278 ''' 279 log = util_log.get_logger() 280 281 for regex in patterns: 282 match = re.search(regex, text) 283 if not match: 284 raise self.TestFail('Cannot match the regexp "{0}" in ' 285 'the output'.format(regex)) 286 else: 287 msg = 'Found match to regex {0}: {1}'.format(regex, 288 match.group()) 289 log.debug(msg) 290 291 @staticmethod 292 def get_tmp_file_path(): 293 '''Get the path of a temporary file that is then deleted. 294 295 Returns: 296 A string that is the path to a temporary file. 297 ''' 298 file_desc, name = tempfile.mkstemp() 299 os.close(file_desc) 300 os.remove(name) 301 return name 302 303 304class TestBaseNoTargetProcess(TestBase): 305 '''lldb target that doesn't require a binary to be running.''' 306 307 def get_bundle_target(self): 308 '''Get bundle executable to run. 309 310 Returns: None 311 ''' 312 return None 313 314 @property 315 def bundle_target(self): 316 return self.get_bundle_target() 317 318 def run(self, dbg, remote_pid, lldb): 319 '''Execute the test case. 320 321 Args: 322 dbg: The instance of the SBDebugger that is used to test commands. 323 lldb: A handle to the lldb module. 324 325 Returns: 326 True: test passed, False: test failed. 327 ''' 328 self._lldb = lldb 329 self._dbg = dbg 330 self._ci = dbg.GetCommandInterpreter() 331 assert self._ci.IsValid() 332 return super(TestBaseNoTargetProcess, self).run(self, dbg, remote_pid) 333