########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/TestSuite/Tester.py,v 1.13 2004/11/04 05:33:46 mbrown Exp $
"""
Provides the Tester class, which is the hub for all testing.
Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""
import sys, os, time
import traceback, linecache
from distutils import spawn
from sys import _getframe
from Ft.Lib import number,Terminal
from Ft.Lib.Terminal import AnsiEscapes
###########################################
#
# Verbosity Levels
# 4 print out group headers, all test names and test results and debug messges
# 3 print out group headers, all test names and test results
# 2 print out group headers and errors and warnings
# 1 print out group headers and errors
# 0 Nothing
VERBOSE_DEBUG = 4
VERBOSE_MSG = 3
VERBOSE_WARN = 2
VERBOSE_ERROR = 1
VERBOSE_OFF = 0
SHOW_TESTS = VERBOSE_MSG
SHOW_GROUPS = VERBOSE_ERROR
_group_headers = ['#', '*', '=', ':', '%', '+', '-','@','$','&']
def _frame_lineno(frame):
"""
Calculate correct line number of stack frame given in frame.
"""
code = frame.f_code
if not hasattr(code, 'co_lnotab'):
return frame.f_lineno
tab = code.co_lnotab
line = code.co_firstlineno
stopat = frame.f_lasti
addr = 0
for i in range(0, len(tab), 2):
addr = addr + ord(tab[i])
if addr > stopat:
break
line = line + ord(tab[i+1])
return line
def extract_stack(frame=None, limit=None):
if frame is None:
# Skip this function body
frame = _getframe(1)
if limit is None:
limit = getattr(sys, 'tracebacklimit', 1000)
stack = []
while frame and limit > 0:
lineno = _frame_lineno(frame)
co = frame.f_code
filename = co.co_filename
name = co.co_name
line = linecache.getline(filename, lineno)
if line:
line = line.strip()
else:
line = None
stack.append((filename, lineno, name, line))
frame = frame.f_back
limit -= 1
stack.reverse()
return stack
def format_stack(frame=None, limit=None):
"""Shorthand for 'format_list(extract_stack(f, limit))'."""
return traceback.format_list(extract_stack(frame, limit))
class TestItem:
def __init__(self, title):
self.title = title
self.messages = []
self.hasErrors = 0
self.hasWarnings = 0
self.comparisons = 0
self.compareTime = 0.0
self.totalTime = 0.0
self.runTime = 0.0
self.startTime = time.time()
return
def __repr__(self):
return '<%s, title=%r, compares=%d>' % (self.__class__.__name__,
self.title,
self.comparisons)
def debug(self, msg):
if msg:
self.messages.append((VERBOSE_DEBUG, msg))
return
def message(self, msg):
if msg:
self.messages.append((VERBOSE_MSG, msg))
return
def warning(self, msg):
self.hasWarnings = 1
if msg:
self.messages.append((VERBOSE_MSG, msg))
return
def error(self, msg):
self.hasErrors = 1
if msg:
self.messages.append((VERBOSE_ERROR, msg))
return
def finish(self):
self.totalTime = time.time() - self.startTime
self.runTime = self.totalTime - self.compareTime
return
class Tester:
double_sep = '=' * 72
single_sep = '-' * 72
NORMAL = AnsiEscapes.Colors.DEFAULT
GRAY = AnsiEscapes.Colors.FOREGROUND_GRAY
RED = AnsiEscapes.Colors.FOREGROUND_RED
GREEN = AnsiEscapes.Colors.FOREGROUND_LIME
BROWN = AnsiEscapes.Colors.FOREGROUND_BROWN
YELLOW = AnsiEscapes.Colors.FOREGROUND_YELLOW
WHITE = AnsiEscapes.Colors.FOREGROUND_WHITE
def __init__(self, stopOnError=1, useColor=1, verbose=VERBOSE_DEBUG,
stream=sys.stdout):
self.stopOnError = stopOnError
self.verbose = verbose
self.stream = stream
self.tty = Terminal.Terminal(stream, keepAnsiEscapes=useColor)
self._writetty = self.tty.writetty
self.test_data = {}
self.groups = []
self.test = None
self.testTime = 0.0
self._diffCommand = spawn.find_executable('diff')
self._compareCtr = 0
# Reporting information
self.warnings = []
self.failures = []
self.exceptions = []
self.totalGroups = 0
self.totalTests = 0
self.totalComparisons = 0
return
### Testing Methods ###
def startGroup(self, title):
header = _group_headers[len(self.groups)]*10
self.writeline(SHOW_GROUPS, '%s %s %s' % (header, title, header))
self.groups.append(title)
self.totalGroups += 1
return
def groupDone(self):
if self.groups:
del self.groups[-1]
else:
self.warning('groupDone called without active group')
return
def startTest(self, title):
if self.test:
# Add warning to current test
self.warning('testDone not called')
self.testDone()
self.test = TestItem(title)
# Add the warning to the new test as well
self.warning('startTest called with active test')
else:
self.test = TestItem(title)
self.totalTests += 1
return
def testDone(self):
if not self.test:
self.warning('testDone called without active test')
return
self.test.finish()
if self.test.hasErrors:
status = '[%sFAILED%s]' % (self.RED,
self.NORMAL)
level = VERBOSE_ERROR
elif self.test.hasWarnings:
status = '[%s WARN %s]' % (self.YELLOW,
self.NORMAL)
level = VERBOSE_WARN
else:
status = '[%s OK %s]' % (self.GREEN,
self.NORMAL)
level = SHOW_TESTS
if not self.test.comparisons:
title = '%s (%0.3f secs)' % (self.test.title,
self.test.totalTime)
else:
title = '%s (%0.3f run, %0.3f total)' % (self.test.title,
self.test.runTime,
self.test.totalTime)
spaces = self.tty.columns() - 9 # length of status
if len(title) > spaces:
self.writeline(level, title)
self.writeline(level, ' '*spaces + status)
else:
self.writeline(level, title.ljust(spaces) + status)
# Write the messages in the queue
for level, line in self.test.messages:
self.writeline(level, line)
self.testTime += self.test.totalTime
self.test = None
return
# -- result testing ----------------------------------------------
def testException(self, func, args, etype, value={}, stackLevel=1, kwargs={}):
# Increase stack trimming to include this function call
stackLevel = stackLevel + 1
try:
func(*args, **kwargs)
except etype, e:
for attr, expected in value.items():
if hasattr(e, attr):
self.compare(expected, getattr(e, attr),
'exception attribute %s' % repr(attr),
stackLevel=stackLevel)
except:
self.exception("Wrong exception raised")
else:
self.error("Expected exception '%s' not raised" % etype,
stackLevel=stackLevel)
return
def compare(self, expected, actual, msg=None, func=cmp, diff=0, stackLevel=1, funcArgs={}):
"""
Uses func to compare the expected result with actual result
of a regression test.
diff is ignored.
msg is an optional custom message to print if the
comparison tests positive (i.e. the results differ).
func is the comparison function to use, and must be a
function that returns the same as the built-in cmp().
stackLevel affects exception reporting.
funcArgs is an optional dictionary of keyword arguments that
will be passed to the comparison function, if the dictionary
is not empty.
"""
self.totalComparisons += 1
# Normalize float values
if type(expected) == type(actual) == float:
if number.finite(expected):
expected = float(str(expected))
elif number.isnan(expected):
expected = 'NaN'
elif number.isinf(expected) > 0:
expected = 'Inf'
else:
expected = '-Inf'
if number.finite(actual):
actual = float(str(actual))
elif number.isnan(actual):
actual = 'NaN'
elif number.isinf(actual) > 0:
actual = 'Inf'
else:
actual = '-Inf'
# Make sure there was a message for this comparison
if not msg:
if self.test:
self.test.comparisons += 1
msg = 'Test %d' % (self.test.comparisons)
else:
msg = 'Test %d of all tests' % self.totalComparisons
start = time.time()
try:
if funcArgs:
res = func(expected, actual, **funcArgs)
else:
res = func(expected, actual)
if res:
# failure
self.message(msg)
if diff and self.verbose >= VERBOSE_DEBUG:
self._diff(expected, actual)
error = '%sExpected:%s %s\n' % (self.GREEN,
self.NORMAL,
repr(expected))
error += '%sCompared:%s %s' % (self.RED,
self.NORMAL,
repr(actual))
self.error(error, stackLevel=(stackLevel+1))
return 0
finally:
end = time.time()
if self.test:
self.test.compareTime += (end - start)
# success
return 1
def compareIn(self, expected, actual, msg=None, stackLevel=1):
"""Test that 'actual' is in 'expected'"""
func = lambda expected, actual: actual not in expected
return self.compare(expected, actual, msg, func,
stackLevel=(stackLevel+1))
# -- display functions -------------------------------------------
def writeline(self, level, msg):
if self.verbose >= level:
self._writetty(msg)
self._writetty('\n')
return
def debug(self, msg):
"""debug-level messages"""
if self.test:
self.test.debug(msg)
else:
self.writeline(VERBOSE_DEBUG, msg)
return
def message(self, msg):
"""informational"""
if self.test:
self.test.message(msg)
else:
self.writeline(VERBOSE_MSG, msg)
return
def warning(self, msg):
"""warning conditions"""
titles = self.groups[:]
if self.test:
titles.append(self.test.title)
self.test.warning(msg)
else:
self.writeline(VERBOSE_WARN, msg)
self.warnings.append((titles, msg))
return
def error(self, msg, traceLimit=1, stackLevel=1):
"""error conditions"""
if self.stopOnError:
traceLimit = getattr(sys, 'tracebacklimit', 1000)
# Trim off 'stackLevel' items from the frame stack.
frame = _getframe(stackLevel)
# Format only 'traceLimit' items in the stack
lines = format_stack(frame, traceLimit)
msg += '\n' + ''.join(lines)
titles = self.groups[:]
if self.test:
titles.append(self.test.title)
self.test.error(msg)
else:
self.writeline(VERBOSE_ERROR, msg)
self.failures.append((titles, msg))
if self.stopOnError:
self.testDone()
raise SystemExit(1)
return
def exception(self, msg):
"""system is unusable""" # ??? looks like it's being used anyway
if not sys.exc_info()[2]:
raise AttributeError('No exception; use error method instead')
try:
etype, value, tb = sys.exc_info()
lines = traceback.format_exception(etype, value, tb)
finally:
etype = value = tb = None
msg += '\n' + ''.join(lines)
titles = self.groups[:]
if self.test:
titles.append(self.test.title)
self.test.error(msg)
else:
self.writeline(VERBOSE_ERROR, msg)
self.exceptions.append((titles, msg))
if self.stopOnError:
self.testDone()
raise SystemExit(1)
return
def _displayList(self, title, color, list):
for titles, message in list:
header = ': '.join(titles)
self._writetty('\n%s%s: %s%s\n' % (color, title, header,
self.NORMAL))
self._writetty(message)
self._writetty('\n')
if list:
self._writetty('\n%s\n' % self.single_sep)
return
def report(self):
self._writetty('\n')
self._writetty(self.double_sep)
self._writetty('\n')
self._displayList('WARN', self.YELLOW, self.warnings)
self._displayList('FAIL', self.RED, self.failures)
self._displayList('EXCEPTION', self.WHITE, self.exceptions)
self._writetty('\n')
self._writetty(" Test Groups Run: %d\n" % self.totalGroups)
self._writetty(" Test Items Run: %d\n" % self.totalTests)
self._writetty(" Results Compared: %d\n" % self.totalComparisons)
self._writetty("Total Time To Run: %0.3fs\n" % self.testTime)
self._writetty('\n')
# -- internal functions ------------------------------------------
def _diff(self, expected, compared):
# get the temporary file directory
import tempfile
tempdir = tempfile.gettempdir()
if self._diffCommand:
# create the expected output file
expected_file = os.path.join(tempdir, 'expected')
fd = open(expected_file, 'w')
fd.write(str(expected))
fd.close()
# create the compared output file
compared_file = os.path.join(tempdir, 'compared')
fd = open(compared_file, 'w')
fd.write(str(compared))
fd.close()
# run diff, capturing stdout and stderr
cmdline = '%s -u %s %s' % (self._diffCommand, expected_file,
compared_file)
self.debug(cmdline)
f_in, f_out = os.popen4(cmdline)
f_in.close()
self.debug(f_out.read())
f_out.close()
os.unlink(expected_file)
os.unlink(compared_file)
return
|