前一篇写到给unittest 通过装饰器增加截图功能。现在还想再HTMLTestRunner中,让截图出现在报告里。
结果如图:
点击可打开连接
实现步骤如下:
于是先在原来的装饰器中把图片的名字输出出来:
def add(func):
PATH = lambda p: os.path.abspath(p)
def wrapper(self, first, second, msg=None):
try:
func(self, first, second, msg=None)
except AssertionError:
path = PATH(os.getcwd() + "/screenshot")
timestamp = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time()))
os.popen("adb wait-for-device")
os.popen("adb shell screencap -p /data/local/tmp/tmp.png")
if not os.path.isdir(PATH(os.getcwd() + "/screenshot")):
os.makedirs(path)
os.popen("adb pull /data/local/tmp/tmp.png " + PATH(path + "/" + timestamp + ".png"))
os.popen("adb shell rm /data/local/tmp/tmp.png")
#打印出路径
print timestamp , '.png'
raise AssertionError,msg
return wrapper
修改处如下:
REPORT_TMPL = """
<p id='show_detail_line'>Show
<a href='javascript:showCase(0)'>Summarya>
<a href='javascript:showCase(1)'>Faileda>
<a href='javascript:showCase(2)'>Alla>
p>
<table id='result_table'>
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
colgroup>
<tr id='header_row'>
<td>Test Group/Test casetd>
<td>Counttd>
<td>Passtd>
<td>Failtd>
<td>Errortd>
<td>Skiptd>
<td>Viewtd>
<td>Screenshottd>#增加一列
tr>
%(test_list)s
<tr id='total_row'>
<td>Totaltd>
<td>%(count)std>
<td>%(Pass)std>
<td>%(fail)std>
<td>%(error)std>
<td>%(skip)std>
<td> td>
<td> <a href="" target="_blank">a>td>
tr>
table>
""" # variables: (test_list, count, Pass, fail, error)
下两处增加了图片行
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)sdiv>td>
<td colspan='6' align='center'>
<a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
%(status)sa>
<div id='div_%(tid)s' class="popup_window" >
<div style='text-align: right; color:red;cursor:pointer'>
<a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
[x]a>
div>
<pre>
%(script)s
pre>
div>
td>
<td align='center'>
<a %(hidde)s href="%(image)s">picture_shota>
td>
tr>
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)sdiv>td>
<td colspan='6' align='center'>%(status)std>
<td align='center'>
<a %(hidde)s href="%(image)s">picture_shota>
td>
tr>
""" # variables: (tid, Class, style, desc, status)
以下方法中加入插入图片的代码段。
通过debug方式运行一下程序可知,此方法中的o,e:
o:在testcase中print 出来的内容,即,我在装饰器中打印出来的图片地址
e:exception的内容,一些出错信息。
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
# e.g. 'pt1.1', 'ft1.1', etc
has_output = bool(o or e)
tid = (n == 0 and 'p' or n == 3 and 's' 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
uo1 = ""
# o and e should be byte string because they are collected from stdout and stderr?
if isinstance(o, str):
uo = str(o)
else:
uo = e
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
else:
ue = o
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
id=tid,
output=saxutils.escape(str(uo) + str(ue))
# output =str(uo) + str(ue)
)
#插入图片
unum = str(uo).find('screenshot:')
if (uo and ue and unum!=-1):
hidde_status = ''
image_url = '../screenshot/'+str(uo)[unum+11:unum+36].replace(' ','')
else:
hidde_status = '''hidden="hidden"'''
image_url = ''
row = tmpl % dict(
tid=tid,
Class=(n == 0 and 'hiddenRow' or 'none'),
style=n == 2 and 'errorCase' or (n == 1 and 'failCase') or (n == 3 and 'skipCase' or 'none'),
desc=desc,
script=script,
hidde=hidde_status,
image=image_url,
status=self.STATUS[n],
)
rows.append(row)
if not has_output:
return
考虑到上文中采用if (uo and ue):的判断,在uo的输出中,可能会有出来screenshot以外的输出内容,所以,在uo转变为url的时候,再加入一个字符串匹配:
先修改一下装饰器中的输出:
def add(func):
PATH = lambda p: os.path.abspath(p)
def wrapper(self, first, second, msg=None):
try:
func(self, first, second, msg=None)
except AssertionError:
path = PATH(os.getcwd() + "/screenshot")
timestamp = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time()))
os.popen("adb wait-for-device")
os.popen("adb shell screencap -p /data/local/tmp/tmp.png")
if not os.path.isdir(PATH(os.getcwd() + "/screenshot")):
os.makedirs(path)
os.popen("adb pull /data/local/tmp/tmp.png " + PATH(path + "/" + timestamp + ".png"))
os.popen("adb shell rm /data/local/tmp/tmp.png")
#打印出路径
print 'screenshot:',timestamp , '.png'
raise AssertionError,msg
return wrapper
接着在_generate_report_test(self, rows, cid, tid, n, t, o, e):方法中修改一部分插入图片的代码:
#插入图片
# if "shot_picture_name" in str(saxutils.escape(str(ue))):
if (uo and ue):
hidde_status = ''
unum=str(uo).find('screenshot:')
image_url = '../screenshot/'+str(uo)[unum+11:unum+36].replace(' ','')
else:
hidde_status = '''hidden="hidden"'''
image_url = ''
row = tmpl % dict(
tid=tid,
Class=(n == 0 and 'hiddenRow' or 'none'),
style=n == 2 and 'errorCase' or (n == 1 and 'failCase') or (n == 3 and 'skipCase' or 'none'),
desc=desc,
script=script,
hidde=hidde_status,
image=image_url,
status=self.STATUS[n],
)
现在无论输出多少内容,都能匹配到以screenshot开头的,形如:2017-06-23-14-28-42.png。字符串作为图片路径啦。。。。
以下是全文代码。参考了[Python]深入解读Python的unittest并拓展HTMLTestRunner中的修改
# coding=utf-8
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.
The simplest way to use this is to invoke its main method. E.g.
import unittest
import HTMLTestRunner
... define your tests ...
if __name__ == '__main__':
HTMLTestRunner.main()
For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
# output to a file
fp = file('my_report.html', 'wb')
runner = HTMLTestRunner.HTMLTestRunner(
stream=fp,
title='My unit test',
description='This demonstrates the report output by HTMLTestRunner.'
)
# Use an external stylesheet.
# See the Template_mixin class for more customizable options
runner.STYLESHEET_TMPL = ''
# run the test
runner.run(my_test_suite)
------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
__author__ = "envony"
__version__ = "0.8.2"
# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
# coding=utf-8
import datetime
import io
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import time
import unittest
import re
from xml.sax import saxutils
# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
# >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
# >>>
class OutputRedirector(object):
""" Wrapper to redirect stdout or stderr """
def __init__(self, fp):
self.fp = fp
def write(self, s):
self.fp.write(s)
def writelines(self, lines):
self.fp.writelines(lines)
def flush(self):
self.fp.flush()
stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)
# ----------------------------------------------------------------------
# Template
class Template_mixin(object):
"""
Define a HTML template for report customerization and generation.
Overall structure of an HTML report
HTML
+------------------------+
| |
| |
| |
| STYLESHEET |
| +----------------+ |
| | | |
| +----------------+ |
| |
| |
| |
| |
| |
| HEADING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| REPORT |
| +----------------+ |
| | | |
| +----------------+ |
| |
| ENDING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| |
| |
+------------------------+
"""
STATUS = {
0: 'pass',
1: 'fail',
2: 'error',
3: 'skip'
}
DEFAULT_TITLE = 'Unit Test Report'
DEFAULT_DESCRIPTION = ''
# ------------------------------------------------------------------------
# HTML Template
HTML_TMPL = r"""
%(title)s
%(stylesheet)s
%(heading)s
%(report)s
%(ending)s
"""
# variables: (title, generator, stylesheet, heading, report, ending)
# ------------------------------------------------------------------------
# Stylesheet
#
# alternatively use a for external style sheet, e.g.
#
STYLESHEET_TMPL = """
"""
# ------------------------------------------------------------------------
# Heading
#
HEADING_TMPL = """
%(title)s
%(parameters)s
%(description)s
""" # variables: (title, parameters, description)
HEADING_ATTRIBUTE_TMPL = """%(name)s: %(value)s
""" # variables: (name, value)
# ------------------------------------------------------------------------
# Report
#
REPORT_TMPL = """
Test Group/Test case
Count
Pass
Fail
Error
Skip
View
Screenshot
%(test_list)s
Total
%(count)s
%(Pass)s
%(fail)s
%(error)s
%(skip)s
""" # variables: (test_list, count, Pass, fail, error)
REPORT_CLASS_TMPL = r"""
%(desc)s
%(count)s
%(Pass)s
%(fail)s
%(error)s
%(skip)s
Detail
""" # variables: (style, desc, count, Pass, fail,skip, error, cid)
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
%(desc)s
%(status)s
%(script)s
picture_shot
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL = r"""
%(desc)s
%(status)s
picture_shot
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
"""
# variables: (id, output)
# ------------------------------------------------------------------------
# ENDING
#
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.
def __init__(self, verbosity=1):
TestResult.__init__(self)
self.stdout0 = None
self.stderr0 = None
self.success_count = 0
self.skipped_count = 0 # add skipped_count
self.failure_count = 0
self.error_count = 0
self.verbosity = verbosity
# result is a list of result in 4 tuple
# (
# result code (0: success; 1: fail; 2: error),
# TestCase object,
# Test output (byte string),
# stack trace,
# )
self.result = []
def startTest(self, test):
TestResult.startTest(self, test)
# just one buffer for both stdout and stderr
self.outputBuffer = io.BytesIO()
stdout_redirector.fp = self.outputBuffer
stderr_redirector.fp = self.outputBuffer
self.stdout0 = sys.stdout
self.stderr0 = sys.stderr
sys.stdout = stdout_redirector
sys.stderr = stderr_redirector
def complete_output(self):
"""
Disconnect output redirection and return buffer.
Safe to call multiple times.
"""
if self.stdout0:
sys.stdout = self.stdout0
sys.stderr = self.stderr0
self.stdout0 = None
self.stderr0 = None
return self.outputBuffer.getvalue()
def stopTest(self, test):
# Usually one of addSuccess, addError or addFailure would have been called.
# But there are some path in unittest that would bypass this.
# 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, ''))
if self.verbosity > 1:
sys.stderr.write('ok ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
else:
sys.stderr.write('.')
def addSkip(self, test, reason):
self.skipped_count += 1
TestResult.addSkip(self, test, reason)
output = self.complete_output()
self.result.append((3, test, '', reason))
if self.verbosity > 1:
sys.stderr.write('skip ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
else:
sys.stderr.write('s')
def addError(self, test, err):
self.error_count += 1
TestResult.addError(self, test, err)
_, _exc_str = self.errors[-1]
output = self.complete_output()
self.result.append((2, test, output, _exc_str))
if self.verbosity > 1:
sys.stderr.write('E ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
else:
sys.stderr.write('E')
def addFailure(self, test, err):
self.failure_count += 1
TestResult.addFailure(self, test, err)
_, _exc_str = self.failures[-1]
output = self.complete_output()
self.result.append((1, test, output, _exc_str))
if self.verbosity > 1:
sys.stderr.write('F ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
else:
sys.stderr.write('F')
class HTMLTestRunner(Template_mixin):
"""
"""
def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None, name=None):
self.stream = stream
self.verbosity = verbosity
if title is None:
self.title = self.DEFAULT_TITLE
else:
self.title = title
if name is None:
self.name = ''
else:
self.name = name
if description is None:
self.description = self.DEFAULT_DESCRIPTION
else:
self.description = description
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))
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:
cls = t.__class__
if not cls in rmap:
rmap[cls] = []
classes.append(cls)
rmap[cls].append((n, t, o, e))
r = [(cls, rmap[cls]) for cls in classes]
return r
def getReportAttributes(self, result):
"""
Return report attributes as a list of (name, value).
Override this to add custom attributes.
"""
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.failure_count: status.append('Failure %s' % result.failure_count)
if result.skipped_count: status.append('Skip %s' % result.skipped_count)
if result.error_count: status.append('Error %s' % result.error_count)
if status:
status = ' '.join(status)
else:
status = 'none'
return [
('Start Time', startTime),
('Duration', duration),
('Status', status),
]
def generateReport(self, test, result):
report_attrs = self.getReportAttributes(result) # 报告的头部
generator = 'HTMLTestRunner %s' % __version__
stylesheet = self._generate_stylesheet() # 拿到css文件
heading = self._generate_heading(report_attrs)
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,
)
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),
# value=value
)
a_lines.append(line)
heading = self.HEADING_TMPL % dict(
title=saxutils.escape(self.title),
parameters=''.join(a_lines),
description=saxutils.escape(self.description),
)
return heading
# 根据result收集报告
def _generate_report(self, result):
rows = []
sortedResult = self.sortResult(result.result)
i = 0
for cid, (cls, cls_results) in enumerate(sortedResult):
# subtotal for a class
np = nf = ns = ne = 0 # np代表pass个数,nf代表fail,ns代表skip,ne,代表error
for n, t, o, e in cls_results:
if n == 0:
np += 1
elif n == 1:
nf += 1
elif n == 3:
ns += 1
else:
ne += 1
# format class description
# if cls.__module__ == "__main__":
# name = cls.__name__
# else:
# name = "%s.%s" % (cls.__module__, cls.__name__)
name = cls.__name__
try:
core_name = self.name[i]
except Exception, e:
core_name = ''
# doc = (cls.__doc__)+core_name and (cls.__doc__+core_name).split("\n")[0] or ""
doc = (cls.__doc__) and cls.__doc__.split("\n")[0] or ""
desc = doc and '%s: %s' % (name, doc) or name
i = i + 1 # 生成每个TestCase类的汇总数据,对于报告中的
row = self.REPORT_CLASS_TMPL % dict(
style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
desc=desc,
count=np + nf + ne + ns,
Pass=np,
fail=nf,
error=ne,
skip=ns,
cid='c%s' % (cid + 1),
)
rows.append(row)
# 生成每个TestCase类中所有方法的测试结果
for tid, (n, t, o, e) in enumerate(cls_results):
self._generate_report_test(rows, cid, tid, n, t, o, e)
report = self.REPORT_TMPL % dict(
test_list=''.join(rows),
count=str(result.success_count + result.failure_count + result.error_count + result.skipped_count),
Pass=str(result.success_count),
fail=str(result.failure_count),
error=str(result.error_count),
skip=str(result.skipped_count)
)
return report
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
# e.g. 'pt1.1', 'ft1.1', etc
has_output = bool(o or e)
tid = (n == 0 and 'p' or n == 3 and 's' 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
uo1 = ""
# o and e should be byte string because they are collected from stdout and stderr?
if isinstance(o, str):
uo = str(o)
else:
uo = e
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
else:
ue = o
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
id=tid,
output=saxutils.escape(str(uo) + str(ue))
# output =str(uo) + str(ue)
)
#插入图片
unum = str(uo).find('screenshot:')
if (uo and ue and unum!=-1):
hidde_status = ''
image_url = '../screenshot/'+str(uo)[unum+11:unum+36].replace(' ','')
else:
hidde_status = '''hidden="hidden"'''
image_url = ''
row = tmpl % dict(
tid=tid,
Class=(n == 0 and 'hiddenRow' or 'none'),
style=n == 2 and 'errorCase' or (n == 1 and 'failCase') or (n == 3 and 'skipCase' or 'none'),
desc=desc,
script=script,
hidde=hidde_status,
image=image_url,
status=self.STATUS[n],
)
rows.append(row)
if not has_output:
return
def _generate_ending(self):
return self.ENDING_TMPL
##############################################################################
# Facilities for running tests from the command line
##############################################################################
# Note: Reuse unittest.TestProgram to launch test. In the future we may
# build our own launcher to support more specific command line
# parameters like test title, CSS, etc.
# 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
# # we have to instantiate HTMLTestRunner before we know self.verbosity.
# if self.testRunner is None:
# self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
# unittest.TestProgram.runTests(self)
#
# main = TestProgram
##############################################################################
# Executing this module from the command line
##############################################################################
if __name__ == "__main__":
main(module=None)