diff --git a/HTMLTestRunner.py b/HTMLTestRunner.py
old mode 100644
new mode 100755
index 8d60600..696ee16
--- a/HTMLTestRunner.py
+++ b/HTMLTestRunner.py
@@ -65,11 +65,12 @@
# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
__author__ = "Wai Yip Tung"
-__version__ = "0.8.3"
-
+__version__ = "0.8.4"
"""
Change History
+Version 0.8.4
+* Collect and show time cost of each test case (Weiqiang Bu).
Version 0.8.3
* Prevent crash on class or module-level exceptions (Darren Wurf).
@@ -119,8 +120,10 @@ def to_unicode(s):
# s is non ascii byte string
return s.decode('unicode_escape')
+
class OutputRedirector(object):
""" Wrapper to redirect stdout or stderr """
+
def __init__(self, fp):
self.fp = fp
@@ -134,11 +137,11 @@ def writelines(self, lines):
def flush(self):
self.fp.flush()
+
stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)
-
# ----------------------------------------------------------------------
# Template
@@ -183,9 +186,9 @@ class Template_mixin(object):
"""
STATUS = {
- 0: 'pass',
- 1: 'fail',
- 2: 'error',
+ 0: 'pass',
+ 1: 'fail',
+ 2: 'error',
}
DEFAULT_TITLE = 'Unit Test Report'
@@ -401,8 +404,6 @@ class Template_mixin(object):
"""
-
-
# ------------------------------------------------------------------------
# Heading
#
@@ -413,12 +414,10 @@ class Template_mixin(object):
%(description)s
-""" # variables: (title, parameters, description)
+""" # variables: (title, parameters, description)
HEADING_ATTRIBUTE_TMPL = """%(name)s: %(value)s
-""" # variables: (name, value)
-
-
+""" # variables: (name, value)
# ------------------------------------------------------------------------
# Report
@@ -457,7 +456,7 @@ class Template_mixin(object):
|
-""" # variables: (test_list, count, Pass, fail, error)
+""" # variables: (test_list, count, Pass, fail, error)
REPORT_CLASS_TMPL = r"""
@@ -468,13 +467,12 @@ class Template_mixin(object):
| %(error)s |
Detail |
-""" # variables: (style, desc, count, Pass, fail, error, cid)
-
+""" # variables: (style, desc, count, Pass, fail, error, cid)
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
%(desc)s |
-
+ |
|
+ %(cost)s |
-""" # variables: (tid, Class, style, desc, status)
-
+""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL = r"""
%(desc)s |
- %(status)s |
+ %(status)s |
+ %(cost)s |
-""" # variables: (tid, Class, style, desc, status)
-
+""" # variables: (tid, Class, style, desc, status,cost)
REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
-""" # variables: (id, output)
-
-
+""" # variables: (id, output)
# ------------------------------------------------------------------------
# ENDING
@@ -516,11 +512,13 @@ class Template_mixin(object):
ENDING_TMPL = """
"""
+
# -------------------- The end of the Template class -------------------
TestResult = unittest.TestResult
+
class _TestResult(TestResult):
# note: _TestResult is a pure representation of results.
# It lacks the output and reporting ability compares to unittest._TextTestResult.
@@ -534,6 +532,8 @@ def __init__(self, verbosity=1):
self.failure_count = 0
self.error_count = 0
self.verbosity = verbosity
+ self.start_time = 0
+ self.time_cost = 0
# result is a list of result in 4 tuple
# (
@@ -544,7 +544,6 @@ def __init__(self, verbosity=1):
# )
self.result = []
-
def startTest(self, test):
TestResult.startTest(self, test)
# just one buffer for both stdout and stderr
@@ -554,7 +553,7 @@ def startTest(self, test):
self.stderr0 = sys.stderr
sys.stdout = stdout_redirector
sys.stderr = stderr_redirector
-
+ self.start_time = time.time()
def complete_output(self):
"""
@@ -566,8 +565,13 @@ def complete_output(self):
sys.stderr = self.stderr0
self.stdout0 = None
self.stderr0 = None
- return self.outputBuffer.getvalue()
-
+ output = self.outputBuffer.getvalue()
+ # need clear buffer after each test
+ self.outputBuffer.truncate(0)
+ self.outputBuffer.seek(0)
+ # self.outputBuffer.close()
+ # self.outputBuffer = StringIO.StringIO()
+ return output
def stopTest(self, test):
# Usually one of addSuccess, addError or addFailure would have been called.
@@ -575,12 +579,12 @@ def stopTest(self, test):
# We must disconnect stdout in stopTest(), which is guaranteed to be called.
self.complete_output()
-
def addSuccess(self, test):
self.success_count += 1
TestResult.addSuccess(self, test)
output = self.complete_output()
- self.result.append((0, test, output, ''))
+ self.time_cost = '{0} s'.format(round(time.time() - self.start_time, 2))
+ self.result.append((0, test, output, '', self.time_cost))
if self.verbosity > 1:
sys.stderr.write('ok ')
sys.stderr.write(str(test))
@@ -593,7 +597,8 @@ def addError(self, test, err):
TestResult.addError(self, test, err)
_, _exc_str = self.errors[-1]
output = self.complete_output()
- self.result.append((2, test, output, _exc_str))
+ self.time_cost = '{0} s'.format(round(time.time() - self.start_time, 2))
+ self.result.append((2, test, output, _exc_str, self.time_cost))
if self.verbosity > 1:
sys.stderr.write('E ')
sys.stderr.write(str(test))
@@ -606,7 +611,8 @@ def addFailure(self, test, err):
TestResult.addFailure(self, test, err)
_, _exc_str = self.failures[-1]
output = self.complete_output()
- self.result.append((1, test, output, _exc_str))
+ self.time_cost = '{0} s'.format(round(time.time() - self.start_time, 2))
+ self.result.append((1, test, output, _exc_str, self.time_cost))
if self.verbosity > 1:
sys.stderr.write('F ')
sys.stderr.write(str(test))
@@ -618,6 +624,7 @@ def addFailure(self, test, err):
class HTMLTestRunner(Template_mixin):
"""
"""
+
def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
self.stream = stream
self.verbosity = verbosity
@@ -632,32 +639,29 @@ def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None)
self.startTime = datetime.datetime.now()
-
def run(self, test):
"Run the given test case or test suite."
result = _TestResult(self.verbosity)
test(result)
self.stopTime = datetime.datetime.now()
self.generateReport(test, result)
- print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
+ print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime - self.startTime)
return result
-
def sortResult(self, result_list):
# unittest does not seems to run in any particular order.
# Here at least we want to group them together by class.
rmap = {}
classes = []
- for n,t,o,e in result_list:
+ for n, t, o, e, c in result_list:
cls = t.__class__
if not rmap.has_key(cls):
rmap[cls] = []
classes.append(cls)
- rmap[cls].append((n,t,o,e))
+ rmap[cls].append((n, t, o, e, c))
r = [(cls, rmap[cls]) for cls in classes]
return r
-
def getReportAttributes(self, result):
"""
Return report attributes as a list of (name, value).
@@ -666,9 +670,9 @@ def getReportAttributes(self, result):
startTime = str(self.startTime)[:19]
duration = str(self.stopTime - self.startTime)
status = []
- if result.success_count: status.append('Pass %s' % result.success_count)
+ if result.success_count: status.append('Pass %s' % result.success_count)
if result.failure_count: status.append('Failure %s' % result.failure_count)
- if result.error_count: status.append('Error %s' % result.error_count )
+ if result.error_count: status.append('Error %s' % result.error_count)
if status:
status = ' '.join(status)
else:
@@ -679,7 +683,6 @@ def getReportAttributes(self, result):
('Status', status),
]
-
def generateReport(self, test, result):
report_attrs = self.getReportAttributes(result)
generator = 'HTMLTestRunner %s' % __version__
@@ -688,46 +691,46 @@ def generateReport(self, test, result):
report = self._generate_report(result)
ending = self._generate_ending()
output = self.HTML_TMPL % dict(
- title = saxutils.escape(self.title),
- generator = generator,
- stylesheet = stylesheet,
- heading = heading,
- report = report,
- ending = ending,
+ title=saxutils.escape(self.title),
+ generator=generator,
+ stylesheet=stylesheet,
+ heading=heading,
+ report=report,
+ ending=ending,
)
self.stream.write(output.encode('utf8'))
-
def _generate_stylesheet(self):
return self.STYLESHEET_TMPL
-
def _generate_heading(self, report_attrs):
a_lines = []
for name, value in report_attrs:
line = self.HEADING_ATTRIBUTE_TMPL % dict(
- name = saxutils.escape(name),
- value = saxutils.escape(value),
- )
+ name=saxutils.escape(name),
+ value=saxutils.escape(value),
+ )
a_lines.append(line)
heading = self.HEADING_TMPL % dict(
- title = saxutils.escape(self.title),
- parameters = ''.join(a_lines),
- description = saxutils.escape(self.description),
+ title=saxutils.escape(self.title),
+ parameters=''.join(a_lines),
+ description=saxutils.escape(self.description),
)
return heading
-
def _generate_report(self, result):
rows = []
sortedResult = self.sortResult(result.result)
for cid, (cls, cls_results) in enumerate(sortedResult):
# subtotal for a class
np = nf = ne = 0
- for n,t,o,e in cls_results:
- if n == 0: np += 1
- elif n == 1: nf += 1
- else: ne += 1
+ for n, t, o, e, c in cls_results:
+ if n == 0:
+ np += 1
+ elif n == 1:
+ nf += 1
+ else:
+ ne += 1
# format class description
if cls.__module__ == "__main__":
@@ -738,64 +741,66 @@ def _generate_report(self, result):
desc = doc and '%s: %s' % (name, doc) or name
row = self.REPORT_CLASS_TMPL % dict(
- style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
- desc = desc,
- count = np+nf+ne,
- Pass = np,
- fail = nf,
- error = ne,
- cid = 'c%s' % (cid+1),
+ style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
+ desc=desc,
+ count=np + nf + ne,
+ Pass=np,
+ fail=nf,
+ error=ne,
+ cid='c%s' % (cid + 1),
)
rows.append(row)
- for tid, (n,t,o,e) in enumerate(cls_results):
- self._generate_report_test(rows, cid, tid, n, t, o, e)
+ for tid, (n, t, o, e, cost) in enumerate(cls_results):
+ self._generate_report_test(rows, cid, tid, n, t, o, e, cost)
report = self.REPORT_TMPL % dict(
- test_list = ''.join(rows),
- count = str(result.success_count+result.failure_count+result.error_count),
- Pass = str(result.success_count),
- fail = str(result.failure_count),
- error = str(result.error_count),
+ test_list=''.join(rows),
+ count=str(result.success_count + result.failure_count + result.error_count),
+ Pass=str(result.success_count),
+ fail=str(result.failure_count),
+ error=str(result.error_count),
)
return report
-
- def _generate_report_test(self, rows, cid, tid, n, t, o, e):
+ def _generate_report_test(self, rows, cid, tid, n, t, o, e, cost=0):
# e.g. 'pt1.1', 'ft1.1', etc
has_output = bool(o or e)
- tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
+ tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1)
name = t.id().split('.')[-1]
doc = t.shortDescription() or ""
desc = doc and ('%s: %s' % (name, doc)) or name
tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
# o and e should be byte string because they are collected from stdout and stderr?
- if isinstance(o,str):
+ if isinstance(o, str):
# TODO: some problem with 'string_escape': it escape \n and mess up formating
# uo = unicode(o.encode('string_escape'))
- uo = o.decode('latin-1')
+ # uo = o.decode('latin-1')
+ uo = o.decode('utf-8')
else:
uo = o
- if isinstance(e,str):
+ if isinstance(e, str):
# TODO: some problem with 'string_escape': it escape \n and mess up formating
# ue = unicode(e.encode('string_escape'))
- ue = e.decode('latin-1')
+ # ue = e.decode('latin-1')
+ ue = e.decode('utf-8')
else:
ue = e
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
- id = tid,
- output = saxutils.escape(uo+ue),
+ id=tid,
+ output=saxutils.escape(uo + ue),
)
row = tmpl % dict(
- tid = tid,
- Class = (n == 0 and 'hiddenRow' or 'none'),
- style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
- desc = desc,
- script = script,
- status = self.STATUS[n],
+ tid=tid,
+ Class=(n == 0 and 'hiddenRow' or 'none'),
+ style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
+ desc=desc,
+ script=script,
+ status=self.STATUS[n],
+ cost=cost
)
rows.append(row)
if not has_output:
@@ -817,6 +822,7 @@ class TestProgram(unittest.TestProgram):
A variation of the unittest.TestProgram. Please refer to the base
class for command line parameters.
"""
+
def runTests(self):
# Pick HTMLTestRunner as the default test runner.
# base class's testRunner parameter is not useful because it means
@@ -825,6 +831,7 @@ def runTests(self):
self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
unittest.TestProgram.runTests(self)
+
main = TestProgram
##############################################################################