blob: 61913eac81575bddff21814c7d4d21e554e4a94a [file] [log] [blame]
'''Module that contains TestBase, the base class of all tests.'''
import logging
import os
import re
import tempfile
import traceback
from exception import DisconnectedException
from . import util_log
class TestBase(object):
'''Base class for all tests. Provides some common functionality.'''
def __init__(self, device_port, device, timer):
# Keep argument names for documentation purposes. This method is
# overwritten by test_base_remote.
# pylint: disable=unused-argument
self._lldb = None # handle to the lldb module
self._ci = None # instance of the lldb command interpreter for this test
self._timer = timer # timer instance, to check whether the test froze
def get_bundle_target(self):
'''Return string with name of bundle executable to run.
Returns:
String that is the name of the binary that this test should be run
with.
'''
raise NotImplementedError
def test_setup(self, android):
'''Set up environment for the test.
Override to specify commands to be run before the test APK launch.
Useful for setting Android properties or environment variables. See also
the test_shutdown method.
Args:
android: Handler to the android device, see the UtilAndroid class.
'''
pass
def test_shutdown(self, android):
'''Clean up environment after test.
Override this procedure to specify commands to be run after the test has
finished. This method is run regardless the outcome of the test.
Ideally, it should revert the changes introduced by test_setup.
Args:
android: Handler to the android device, see the UtilAndroid class.
'''
pass
def run(self, dbg, remote_pid, lldb, wimpy):
'''Execute the actual test.
Args:
dbg: The instance of the SBDebugger that is used to test commands.
remote_pid: The integer that is the process id of the binary that
the debugger is attached to.
lldb: A handle to the lldb module.
wimpy: Boolean to specify only a subset of the commands be executed.
Returns:
True if the test passed, or False if not.
'''
raise NotImplementedError
def post_run(self):
'''Clean up after test execution.'''
pass
class TestFail(Exception):
'''Exception that is thrown when a line in a test fails.
This exception is thrown if a lldb command does not return the expected
string.
'''
pass
def test_assert(self, cond):
'''Check a given condition and raise TestFail if it is False.
Args:
cond: The boolean condition to check.
Raises:
TestFail: The condition was false.
'''
if not cond:
raise self.TestFail()
def assert_lang_renderscript(self):
'''Check that LLDB is stopped in a RenderScript frame
Use the LLDB API to check that the language of the current frame
is RenderScript, fail otherwise.
Raises:
TestFail: Detected language not RenderScript.
'''
assert self._lldb
assert self._ci
proc = self._ci.GetProcess()
frame = proc.GetSelectedThread().GetSelectedFrame()
lang = frame.GetCompileUnit().GetLanguage()
if lang != self._lldb.eLanguageTypeExtRenderScript:
raise self.TestFail('Frame language not RenderScript, instead {0}'
.format(lang))
def do_command(self, cmd):
'''Run an lldb command and return the output.
Args:
cmd: The string representing the lldb command to run.
Raises:
TestFail: The lldb command failed.
'''
assert self._lldb
assert self._ci
log = util_log.get_logger()
res = self._lldb.SBCommandReturnObject()
log.info('[Command] {0}'.format(cmd))
# before issuing the command, restart the current timer to check
# whether the command is going to freeze the test
if self._timer:
self._timer.reset()
self._ci.HandleCommand(cmd, res)
if not res.Succeeded():
error = res.GetError()
raise self.TestFail('The command "{0}" failed with the error: {1}'
.format(cmd, error if error else '<N/a>'))
output = res.GetOutput()
log.debug('[Output] {0}'.format(output.rstrip()))
return output
def try_command(self, cmd, expected, expected_regex=None):
'''Run an lldb command and match the expected response.
Args:
cmd: The string representing the lldb command to run.
expected: A list of strings that should be present in lldb's
output.
expected_regex: A list of regular expressions that should
match lldb's output.
Raises:
TestFail: One of the expected strings were not found in the lldb
output.
'''
assert self._lldb
assert self._ci
log = util_log.get_logger()
output = ''
try:
output = self.do_command(cmd)
if 'lost connection' in output:
raise DisconnectedException('Lost connection to lldb-server.')
# check the expected strings
self._match_literals(output, expected)
# check the regexp patterns
if expected_regex:
self._match_regexp_patterns(output, expected_regex)
except self.TestFail as exception:
# if the command failed, ensure the output retrieved from the
# command is printed even in verbose mode
if log.getEffectiveLevel() > logging.DEBUG:
log.error('[Output] {0}'.format(output.rstrip() if output
else '<empty>'))
# print the back trace, it should help to identify the error in
# the test
backtrace = ['[Back trace]']
for (filename, line, function, text) in \
traceback.extract_stack()[:-1]:
backtrace.append(' [{0} line: {2} fn: {1}] {3}'.format(
filename, function, line, text
)
)
log.error('\n'.join(backtrace))
log.error('[TEST ERROR] {0}'.format(exception.message))
raise # pass through
def _match_literals(self, text, literals):
'''Checks the text against the array of literals.
Raises a TestFail exception in case one of the literals is not contained
in the text.
Args:
text: String, it represents the text to match.
literals: an array of string literals to match in the output.
Throws: self.TestFail: if it cannot match one of the literals in
the output.
'''
for string in literals:
if string not in text:
raise self.TestFail('Cannot find "{0}" in the output'
.format(string))
def _match_regexp_patterns(self, text, patterns):
'''Checks the text against the array of regular expression patterns.
Raises a TestFail exception in case one of the patterns is not matched
in the given text.
Args:
text: String, it represents the text to match.
patterns: an array of strings, each of them representing a regular
expression to match in text.
Throws: self.TestFail: if it cannot match one of the literals in
the output.
'''
log = util_log.get_logger()
for regex in patterns:
match = re.search(regex, text)
if not match:
raise self.TestFail('Cannot match the regexp "{0}" in '
'the output'.format(regex))
else:
msg = 'Found match to regex {0}: {1}'.format(regex,
match.group())
log.debug(msg)
@staticmethod
def get_tmp_file_path():
'''Get the path of a temporary file that is then deleted.
Returns:
A string that is the path to a temporary file.
'''
file_desc, name = tempfile.mkstemp()
os.close(file_desc)
os.remove(name)
return name