HTMLTestRunner实现报告中的截图

前一篇写到给unittest 通过装饰器增加截图功能。现在还想再HTMLTestRunner中,让截图出现在报告里。
结果如图:
HTMLTestRunner实现报告中的截图_第1张图片
点击可打开连接

实现步骤如下:

于是先在原来的装饰器中把图片的名字输出出来:

    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 = """

Show Summary Failed All

%(test_list)s
Test Group/Test case Count Pass Fail Error Skip View Screenshot
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 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)

你可能感兴趣的:(unittest)