zoukankan      html  css  js  c++  java
  • unittest生成测试报告

    About

    HTMLTestRunner和BSTestRunner是Python标准库unittest的扩展,用来生成HTML的测试报告。两个下载安装和使用一致。

    首先,Python2.x和Python3.x中两个扩展包不兼容(但下载和使用一致),这里以Python3.x为例。只是目前,无法通过pip安装。所以在使用之前,需要下载HTTLTestRunner.py,下载地址在文章最后的链接中。或者将下面的源码拷贝一份,文件名为HTTLTestRunner.py,保存在Python解释器的 Libsite-packages 目录中即可。 BSTestRunner的下载使用参见HTTLTestRunner。

    简单使用

    import webbrowser
    import unittest
    import HTMLTestRunner
    import BSTestRunner
     
     
    class TestStringMethods(unittest.TestCase):
     
        def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO')
     
        def test_isupper(self):
            self.assertTrue('Foo'.isupper())
     
     
    if __name__ == '__main__':
        suite = unittest.makeSuite(TestStringMethods)
        f1 = open('result1.html', 'wb')
        f2 = open('result2.html', 'wb')
        HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(
            suite)
        suite = unittest.makeSuite(TestStringMethods)
        BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite)
        f1.close()
        f2.close()
        webbrowser.open('result1.html')
        webbrowser.open('result2.html')
    View Code

    其中:

    • stream是文件句柄。

    • title是测试报告的title。

    • description是测试报告的描述信息。

    这样在本地就生成了result1.htmlresult2.html两个HTML文件:

     

     OK,还是比较完美的,再来一点优化:

    优化版

    优化其实很简单:

    import webbrowser
    import unittest
    import HTMLTestRunner
    import BSTestRunner
     
     
    class TestStringMethods(unittest.TestCase):
     
        def test_upper(self):
            """判断 foo.upper() 是否等于 FOO"""
            self.assertEqual('foo'.upper(), 'FOO')
     
        def test_isupper(self):
            """ 判断 Foo 是否为大写形式 """
            self.assertTrue('Foo'.isupper())
     
     
    if __name__ == '__main__':
        suite = unittest.makeSuite(TestStringMethods)
        f1 = open('result1.html', 'wb')
        f2 = open('result2.html', 'wb')
        HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(
            suite)
        suite = unittest.makeSuite(TestStringMethods)
        BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite)
        f1.close()
        f2.close()
        webbrowser.open('result1.html')
        webbrowser.open('result2.html')
    View Code

    其实就是为每个用例方法添加上注释说明。

    Python2.x版本

    import webbrowser
    import unittest
    import HTMLTestRunner
    import BSTestRunner
     
     
    class TestStringMethods(unittest.TestCase):
     
        def test_upper(self):
            u"""判断 foo.upper() 是否等于 FOO"""
            self.assertEqual('foo'.upper(), 'FOO')
     
        def test_isupper(self):
            u""" 判断 Foo 是否为大写形式 """
            self.assertTrue('Foo'.isupper())
     
     
    if __name__ == '__main__':
        suite = unittest.makeSuite(TestStringMethods)
        f1 = open('result1.html', 'wb')
        f2 = open('result2.html', 'wb')
        HTMLTestRunner.HTMLTestRunner(
            stream=f1,
            title=u'HTMLTestRunner版本关于upper的测试报告',
            description=u'判断upper的测试用例执行情况').run(suite)
        suite = unittest.makeSuite(TestStringMethods)
        BSTestRunner.BSTestRunner(
            stream=f2,
            title=u'BSTestRunner版本关于upper的测试报告',
            description=u'判断upper的测试用例执行情况').run(suite)
        f1.close()
        f2.close()
        webbrowser.open('result1.html')
        webbrowser.open('result2.html')
    View Code

    Python2.x与Python3.x的用法一致,就是别忘了,中文字符串前面要加u

    各版本的两文件的源码,保存到指定位置即可。

      1 """
      2 A TestRunner for use with the Python unit testing framework. It
      3 generates a HTML report to show the result at a glance.
      4 
      5 The simplest way to use this is to invoke its main method. E.g.
      6 
      7     import unittest
      8     import HTMLTestRunner
      9 
     10     ... define your tests ...
     11 
     12     if __name__ == '__main__':
     13         HTMLTestRunner.main()
     14 
     15 
     16 For more customization options, instantiates a HTMLTestRunner object.
     17 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
     18 
     19     # output to a file
     20     fp = file('my_report.html', 'wb')
     21     runner = HTMLTestRunner.HTMLTestRunner(
     22                 stream=fp,
     23                 title='My unit test',
     24                 description='This demonstrates the report output by HTMLTestRunner.'
     25                 )
     26 
     27     # Use an external stylesheet.
     28     # See the Template_mixin class for more customizable options
     29     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
     30 
     31     # run the test
     32     runner.run(my_test_suite)
     33 
     34 
     35 ------------------------------------------------------------------------
     36 Copyright (c) 2004-2007, Wai Yip Tung
     37 All rights reserved.
     38 
     39 Redistribution and use in source and binary forms, with or without
     40 modification, are permitted provided that the following conditions are
     41 met:
     42 
     43 * Redistributions of source code must retain the above copyright notice,
     44   this list of conditions and the following disclaimer.
     45 * Redistributions in binary form must reproduce the above copyright
     46   notice, this list of conditions and the following disclaimer in the
     47   documentation and/or other materials provided with the distribution.
     48 * Neither the name Wai Yip Tung nor the names of its contributors may be
     49   used to endorse or promote products derived from this software without
     50   specific prior written permission.
     51 
     52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
     55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
     56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     63 """
     64 
     65 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
     66 
     67 __author__ = "Wai Yip Tung"
     68 __version__ = "0.8.2"
     69 
     70 
     71 """
     72 Change History
     73 
     74 Version 0.8.2
     75 * Show output inline instead of popup window (Viorel Lupu).
     76 
     77 Version in 0.8.1
     78 * Validated XHTML (Wolfgang Borgert).
     79 * Added description of test classes and test cases.
     80 
     81 Version in 0.8.0
     82 * Define Template_mixin class for customization.
     83 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
     84 
     85 Version in 0.7.1
     86 * Back port to Python 2.3 (Frank Horowitz).
     87 * Fix missing scroll bars in detail log (Podi).
     88 """
     89 
     90 # TODO: color stderr
     91 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
     92 
     93 import datetime
     94 import io
     95 import sys
     96 import time
     97 import unittest
     98 from xml.sax import saxutils
     99 
    100 
    101 # ------------------------------------------------------------------------
    102 # The redirectors below are used to capture output during testing. Output
    103 # sent to sys.stdout and sys.stderr are automatically captured. However
    104 # in some cases sys.stdout is already cached before HTMLTestRunner is
    105 # invoked (e.g. calling logging.basicConfig). In order to capture those
    106 # output, use the redirectors for the cached stream.
    107 #
    108 # e.g.
    109 #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
    110 #   >>>
    111 
    112 class OutputRedirector(object):
    113     """ Wrapper to redirect stdout or stderr """
    114     def __init__(self, fp):
    115         self.fp = fp
    116 
    117     def write(self, s):
    118         self.fp.write(s)
    119 
    120     def writelines(self, lines):
    121         self.fp.writelines(lines)
    122 
    123     def flush(self):
    124         self.fp.flush()
    125 
    126 stdout_redirector = OutputRedirector(sys.stdout)
    127 stderr_redirector = OutputRedirector(sys.stderr)
    128 
    129 
    130 
    131 # ----------------------------------------------------------------------
    132 # Template
    133 
    134 class Template_mixin(object):
    135     """
    136     Define a HTML template for report customerization and generation.
    137 
    138     Overall structure of an HTML report
    139 
    140     HTML
    141     +------------------------+
    142     |<html>                  |
    143     |  <head>                |
    144     |                        |
    145     |   STYLESHEET           |
    146     |   +----------------+   |
    147     |   |                |   |
    148     |   +----------------+   |
    149     |                        |
    150     |  </head>               |
    151     |                        |
    152     |  <body>                |
    153     |                        |
    154     |   HEADING              |
    155     |   +----------------+   |
    156     |   |                |   |
    157     |   +----------------+   |
    158     |                        |
    159     |   REPORT               |
    160     |   +----------------+   |
    161     |   |                |   |
    162     |   +----------------+   |
    163     |                        |
    164     |   ENDING               |
    165     |   +----------------+   |
    166     |   |                |   |
    167     |   +----------------+   |
    168     |                        |
    169     |  </body>               |
    170     |</html>                 |
    171     +------------------------+
    172     """
    173 
    174     STATUS = {
    175     0: 'pass',
    176     1: 'fail',
    177     2: 'error',
    178     }
    179 
    180     DEFAULT_TITLE = 'Unit Test Report'
    181     DEFAULT_DESCRIPTION = ''
    182 
    183     # ------------------------------------------------------------------------
    184     # HTML Template
    185 
    186     HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
    187 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    188 <html xmlns="http://www.w3.org/1999/xhtml">
    189 <head>
    190     <title>%(title)s</title>
    191     <meta name="generator" content="%(generator)s"/>
    192     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    193     %(stylesheet)s
    194 </head>
    195 <body>
    196 <script language="javascript" type="text/javascript"><!--
    197 output_list = Array();
    198 
    199 /* level - 0:Summary; 1:Failed; 2:All */
    200 function showCase(level) {
    201     trs = document.getElementsByTagName("tr");
    202     for (var i = 0; i < trs.length; i++) {
    203         tr = trs[i];
    204         id = tr.id;
    205         if (id.substr(0,2) == 'ft') {
    206             if (level < 1) {
    207                 tr.className = 'hiddenRow';
    208             }
    209             else {
    210                 tr.className = '';
    211             }
    212         }
    213         if (id.substr(0,2) == 'pt') {
    214             if (level > 1) {
    215                 tr.className = '';
    216             }
    217             else {
    218                 tr.className = 'hiddenRow';
    219             }
    220         }
    221     }
    222 }
    223 
    224 
    225 function showClassDetail(cid, count) {
    226     var id_list = Array(count);
    227     var toHide = 1;
    228     for (var i = 0; i < count; i++) {
    229         tid0 = 't' + cid.substr(1) + '.' + (i+1);
    230         tid = 'f' + tid0;
    231         tr = document.getElementById(tid);
    232         if (!tr) {
    233             tid = 'p' + tid0;
    234             tr = document.getElementById(tid);
    235         }
    236         id_list[i] = tid;
    237         if (tr.className) {
    238             toHide = 0;
    239         }
    240     }
    241     for (var i = 0; i < count; i++) {
    242         tid = id_list[i];
    243         if (toHide) {
    244             document.getElementById('div_'+tid).style.display = 'none'
    245             document.getElementById(tid).className = 'hiddenRow';
    246         }
    247         else {
    248             document.getElementById(tid).className = '';
    249         }
    250     }
    251 }
    252 
    253 
    254 function showTestDetail(div_id){
    255     var details_div = document.getElementById(div_id)
    256     var displayState = details_div.style.display
    257     // alert(displayState)
    258     if (displayState != 'block' ) {
    259         displayState = 'block'
    260         details_div.style.display = 'block'
    261     }
    262     else {
    263         details_div.style.display = 'none'
    264     }
    265 }
    266 
    267 
    268 function html_escape(s) {
    269     s = s.replace(/&/g,'&amp;');
    270     s = s.replace(/</g,'&lt;');
    271     s = s.replace(/>/g,'&gt;');
    272     return s;
    273 }
    274 
    275 /* obsoleted by detail in <div>
    276 function showOutput(id, name) {
    277     var w = window.open("", //url
    278                     name,
    279                     "resizable,scrollbars,status,width=800,height=450");
    280     d = w.document;
    281     d.write("<pre>");
    282     d.write(html_escape(output_list[id]));
    283     d.write("
    ");
    284     d.write("<a href='javascript:window.close()'>close</a>
    ");
    285     d.write("</pre>
    ");
    286     d.close();
    287 }
    288 */
    289 --></script>
    290 
    291 %(heading)s
    292 %(report)s
    293 %(ending)s
    294 
    295 </body>
    296 </html>
    297 """
    298     # variables: (title, generator, stylesheet, heading, report, ending)
    299 
    300 
    301     # ------------------------------------------------------------------------
    302     # Stylesheet
    303     #
    304     # alternatively use a <link> for external style sheet, e.g.
    305     #   <link rel="stylesheet" href="$url" type="text/css">
    306 
    307     STYLESHEET_TMPL = """
    308 <style type="text/css" media="screen">
    309 body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
    310 table       { font-size: 100%; }
    311 pre         { }
    312 
    313 /* -- heading ---------------------------------------------------------------------- */
    314 h1 {
    315     font-size: 16pt;
    316     color: gray;
    317 }
    318 .heading {
    319     margin-top: 0ex;
    320     margin-bottom: 1ex;
    321 }
    322 
    323 .heading .attribute {
    324     margin-top: 1ex;
    325     margin-bottom: 0;
    326 }
    327 
    328 .heading .description {
    329     margin-top: 4ex;
    330     margin-bottom: 6ex;
    331 }
    332 
    333 /* -- css div popup ------------------------------------------------------------------------ */
    334 a.popup_link {
    335 }
    336 
    337 a.popup_link:hover {
    338     color: red;
    339 }
    340 
    341 .popup_window {
    342     display: none;
    343     position: relative;
    344     left: 0px;
    345     top: 0px;
    346     /*border: solid #627173 1px; */
    347     padding: 10px;
    348     background-color: #E6E6D6;
    349     font-family: "Lucida Console", "Courier New", Courier, monospace;
    350     text-align: left;
    351     font-size: 8pt;
    352      500px;
    353 }
    354 
    355 }
    356 /* -- report ------------------------------------------------------------------------ */
    357 #show_detail_line {
    358     margin-top: 3ex;
    359     margin-bottom: 1ex;
    360 }
    361 #result_table {
    362      80%;
    363     border-collapse: collapse;
    364     border: 1px solid #777;
    365 }
    366 #header_row {
    367     font-weight: bold;
    368     color: white;
    369     background-color: #777;
    370 }
    371 #result_table td {
    372     border: 1px solid #777;
    373     padding: 2px;
    374 }
    375 #total_row  { font-weight: bold; }
    376 .passClass  { background-color: #6c6; }
    377 .failClass  { background-color: #c60; }
    378 .errorClass { background-color: #c00; }
    379 .passCase   { color: #6c6; }
    380 .failCase   { color: #c60; font-weight: bold; }
    381 .errorCase  { color: #c00; font-weight: bold; }
    382 .hiddenRow  { display: none; }
    383 .testcase   { margin-left: 2em; }
    384 
    385 
    386 /* -- ending ---------------------------------------------------------------------- */
    387 #ending {
    388 }
    389 
    390 </style>
    391 """
    392 
    393 
    394 
    395     # ------------------------------------------------------------------------
    396     # Heading
    397     #
    398 
    399     HEADING_TMPL = """<div class='heading'>
    400 <h1>%(title)s</h1>
    401 %(parameters)s
    402 <p class='description'>%(description)s</p>
    403 </div>
    404 
    405 """ # variables: (title, parameters, description)
    406 
    407     HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
    408 """ # variables: (name, value)
    409 
    410 
    411 
    412     # ------------------------------------------------------------------------
    413     # Report
    414     #
    415 
    416     REPORT_TMPL = """
    417 <p id='show_detail_line'>Show
    418 <a href='javascript:showCase(0)'>Summary</a>
    419 <a href='javascript:showCase(1)'>Failed</a>
    420 <a href='javascript:showCase(2)'>All</a>
    421 </p>
    422 <table id='result_table'>
    423 <colgroup>
    424 <col align='left' />
    425 <col align='right' />
    426 <col align='right' />
    427 <col align='right' />
    428 <col align='right' />
    429 <col align='right' />
    430 </colgroup>
    431 <tr id='header_row'>
    432     <td>Test Group/Test case</td>
    433     <td>Count</td>
    434     <td>Pass</td>
    435     <td>Fail</td>
    436     <td>Error</td>
    437     <td>View</td>
    438 </tr>
    439 %(test_list)s
    440 <tr id='total_row'>
    441     <td>Total</td>
    442     <td>%(count)s</td>
    443     <td>%(Pass)s</td>
    444     <td>%(fail)s</td>
    445     <td>%(error)s</td>
    446     <td>&nbsp;</td>
    447 </tr>
    448 </table>
    449 """ # variables: (test_list, count, Pass, fail, error)
    450 
    451     REPORT_CLASS_TMPL = r"""
    452 <tr class='%(style)s'>
    453     <td>%(desc)s</td>
    454     <td>%(count)s</td>
    455     <td>%(Pass)s</td>
    456     <td>%(fail)s</td>
    457     <td>%(error)s</td>
    458     <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
    459 </tr>
    460 """ # variables: (style, desc, count, Pass, fail, error, cid)
    461 
    462 
    463     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    464 <tr id='%(tid)s' class='%(Class)s'>
    465     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    466     <td colspan='5' align='center'>
    467 
    468     <!--css div popup start-->
    469     <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
    470         %(status)s</a>
    471 
    472     <div id='div_%(tid)s' class="popup_window">
    473         <div style='text-align: right; color:red;cursor:pointer'>
    474         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
    475            [x]</a>
    476         </div>
    477         <pre>
    478         %(script)s
    479         </pre>
    480     </div>
    481     <!--css div popup end-->
    482 
    483     </td>
    484 </tr>
    485 """ # variables: (tid, Class, style, desc, status)
    486 
    487 
    488     REPORT_TEST_NO_OUTPUT_TMPL = r"""
    489 <tr id='%(tid)s' class='%(Class)s'>
    490     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    491     <td colspan='5' align='center'>%(status)s</td>
    492 </tr>
    493 """ # variables: (tid, Class, style, desc, status)
    494 
    495 
    496     REPORT_TEST_OUTPUT_TMPL = r"""
    497 %(id)s: %(output)s
    498 """ # variables: (id, output)
    499 
    500 
    501 
    502     # ------------------------------------------------------------------------
    503     # ENDING
    504     #
    505 
    506     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    507 
    508 # -------------------- The end of the Template class -------------------
    509 
    510 
    511 TestResult = unittest.TestResult
    512 
    513 class _TestResult(TestResult):
    514     # note: _TestResult is a pure representation of results.
    515     # It lacks the output and reporting ability compares to unittest._TextTestResult.
    516 
    517     def __init__(self, verbosity=1):
    518         TestResult.__init__(self)
    519         self.stdout0 = None
    520         self.stderr0 = None
    521         self.success_count = 0
    522         self.failure_count = 0
    523         self.error_count = 0
    524         self.verbosity = verbosity
    525 
    526         # result is a list of result in 4 tuple
    527         # (
    528         #   result code (0: success; 1: fail; 2: error),
    529         #   TestCase object,
    530         #   Test output (byte string),
    531         #   stack trace,
    532         # )
    533         self.result = []
    534 
    535 
    536     def startTest(self, test):
    537         TestResult.startTest(self, test)
    538         # just one buffer for both stdout and stderr
    539         self.outputBuffer = io.BytesIO()
    540         stdout_redirector.fp = self.outputBuffer
    541         stderr_redirector.fp = self.outputBuffer
    542         self.stdout0 = sys.stdout
    543         self.stderr0 = sys.stderr
    544         sys.stdout = stdout_redirector
    545         sys.stderr = stderr_redirector
    546 
    547 
    548     def complete_output(self):
    549         """
    550         Disconnect output redirection and return buffer.
    551         Safe to call multiple times.
    552         """
    553         if self.stdout0:
    554             sys.stdout = self.stdout0
    555             sys.stderr = self.stderr0
    556             self.stdout0 = None
    557             self.stderr0 = None
    558         return self.outputBuffer.getvalue()
    559 
    560 
    561     def stopTest(self, test):
    562         # Usually one of addSuccess, addError or addFailure would have been called.
    563         # But there are some path in unittest that would bypass this.
    564         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
    565         self.complete_output()
    566 
    567 
    568     def addSuccess(self, test):
    569         self.success_count += 1
    570         TestResult.addSuccess(self, test)
    571         output = self.complete_output()
    572         self.result.append((0, test, output, ''))
    573         if self.verbosity > 1:
    574             sys.stderr.write('ok ')
    575             sys.stderr.write(str(test))
    576             sys.stderr.write('
    ')
    577         else:
    578             sys.stderr.write('.')
    579 
    580     def addError(self, test, err):
    581         self.error_count += 1
    582         TestResult.addError(self, test, err)
    583         _, _exc_str = self.errors[-1]
    584         output = self.complete_output()
    585         self.result.append((2, test, output, _exc_str))
    586         if self.verbosity > 1:
    587             sys.stderr.write('E  ')
    588             sys.stderr.write(str(test))
    589             sys.stderr.write('
    ')
    590         else:
    591             sys.stderr.write('E')
    592 
    593     def addFailure(self, test, err):
    594         self.failure_count += 1
    595         TestResult.addFailure(self, test, err)
    596         _, _exc_str = self.failures[-1]
    597         output = self.complete_output()
    598         self.result.append((1, test, output, _exc_str))
    599         if self.verbosity > 1:
    600             sys.stderr.write('F  ')
    601             sys.stderr.write(str(test))
    602             sys.stderr.write('
    ')
    603         else:
    604             sys.stderr.write('F')
    605 
    606 
    607 class HTMLTestRunner(Template_mixin):
    608     """
    609     """
    610     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
    611         self.stream = stream
    612         self.verbosity = verbosity
    613         if title is None:
    614             self.title = self.DEFAULT_TITLE
    615         else:
    616             self.title = title
    617         if description is None:
    618             self.description = self.DEFAULT_DESCRIPTION
    619         else:
    620             self.description = description
    621 
    622         self.startTime = datetime.datetime.now()
    623 
    624 
    625     def run(self, test):
    626         "Run the given test case or test suite."
    627         result = _TestResult(self.verbosity)
    628         test(result)
    629         self.stopTime = datetime.datetime.now()
    630         self.generateReport(test, result)
    631         print(sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime))
    632         return result
    633 
    634 
    635     def sortResult(self, result_list):
    636         # unittest does not seems to run in any particular order.
    637         # Here at least we want to group them together by class.
    638         rmap = {}
    639         classes = []
    640         for n,t,o,e in result_list:
    641             cls = t.__class__
    642             if not cls in rmap:
    643                 rmap[cls] = []
    644                 classes.append(cls)
    645             rmap[cls].append((n,t,o,e))
    646         r = [(cls, rmap[cls]) for cls in classes]
    647         return r
    648 
    649 
    650     def getReportAttributes(self, result):
    651         """
    652         Return report attributes as a list of (name, value).
    653         Override this to add custom attributes.
    654         """
    655         startTime = str(self.startTime)[:19]
    656         duration = str(self.stopTime - self.startTime)
    657         status = []
    658         if result.success_count: status.append('Pass %s'    % result.success_count)
    659         if result.failure_count: status.append('Failure %s' % result.failure_count)
    660         if result.error_count:   status.append('Error %s'   % result.error_count  )
    661         if status:
    662             status = ' '.join(status)
    663         else:
    664             status = 'none'
    665         return [
    666             ('Start Time', startTime),
    667             ('Duration', duration),
    668             ('Status', status),
    669         ]
    670 
    671 
    672     def generateReport(self, test, result):
    673         report_attrs = self.getReportAttributes(result)
    674         generator = 'HTMLTestRunner %s' % __version__
    675         stylesheet = self._generate_stylesheet()
    676         heading = self._generate_heading(report_attrs)
    677         report = self._generate_report(result)
    678         ending = self._generate_ending()
    679         output = self.HTML_TMPL % dict(
    680             title = saxutils.escape(self.title),
    681             generator = generator,
    682             stylesheet = stylesheet,
    683             heading = heading,
    684             report = report,
    685             ending = ending,
    686         )
    687         self.stream.write(output.encode('utf8'))
    688 
    689 
    690     def _generate_stylesheet(self):
    691         return self.STYLESHEET_TMPL
    692 
    693 
    694     def _generate_heading(self, report_attrs):
    695         a_lines = []
    696         for name, value in report_attrs:
    697             line = self.HEADING_ATTRIBUTE_TMPL % dict(
    698                     name = saxutils.escape(name),
    699                     value = saxutils.escape(value),
    700                 )
    701             a_lines.append(line)
    702         heading = self.HEADING_TMPL % dict(
    703             title = saxutils.escape(self.title),
    704             parameters = ''.join(a_lines),
    705             description = saxutils.escape(self.description),
    706         )
    707         return heading
    708 
    709 
    710     def _generate_report(self, result):
    711         rows = []
    712         sortedResult = self.sortResult(result.result)
    713         for cid, (cls, cls_results) in enumerate(sortedResult):
    714             # subtotal for a class
    715             np = nf = ne = 0
    716             for n,t,o,e in cls_results:
    717                 if n == 0: np += 1
    718                 elif n == 1: nf += 1
    719                 else: ne += 1
    720 
    721             # format class description
    722             if cls.__module__ == "__main__":
    723                 name = cls.__name__
    724             else:
    725                 name = "%s.%s" % (cls.__module__, cls.__name__)
    726             doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
    727             desc = doc and '%s: %s' % (name, doc) or name
    728 
    729             row = self.REPORT_CLASS_TMPL % dict(
    730                 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
    731                 desc = desc,
    732                 count = np+nf+ne,
    733                 Pass = np,
    734                 fail = nf,
    735                 error = ne,
    736                 cid = 'c%s' % (cid+1),
    737             )
    738             rows.append(row)
    739 
    740             for tid, (n,t,o,e) in enumerate(cls_results):
    741                 self._generate_report_test(rows, cid, tid, n, t, o, e)
    742 
    743         report = self.REPORT_TMPL % dict(
    744             test_list = ''.join(rows),
    745             count = str(result.success_count+result.failure_count+result.error_count),
    746             Pass = str(result.success_count),
    747             fail = str(result.failure_count),
    748             error = str(result.error_count),
    749         )
    750         return report
    751 
    752 
    753     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
    754         # e.g. 'pt1.1', 'ft1.1', etc
    755         has_output = bool(o or e)
    756         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
    757         name = t.id().split('.')[-1]
    758         doc = t.shortDescription() or ""
    759         desc = doc and ('%s: %s' % (name, doc)) or name
    760         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    761 
    762         # o and e should be byte string because they are collected from stdout and stderr?
    763         if isinstance(o,str):
    764             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    765             # uo = unicode(o.encode('string_escape'))
    766             uo = o.decode('latin-1')
    767         else:
    768             uo = o
    769         if isinstance(e,str):
    770             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    771             # ue = unicode(e.encode('string_escape'))
    772             ue = e
    773         else:
    774             ue = e
    775 
    776         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
    777             id = tid,
    778             output = saxutils.escape(str(uo)+ue),
    779         )
    780 
    781         row = tmpl % dict(
    782             tid = tid,
    783             Class = (n == 0 and 'hiddenRow' or 'none'),
    784             style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
    785             desc = desc,
    786             script = script,
    787             status = self.STATUS[n],
    788         )
    789         rows.append(row)
    790         if not has_output:
    791             return
    792 
    793     def _generate_ending(self):
    794         return self.ENDING_TMPL
    795 
    796 
    797 ##############################################################################
    798 # Facilities for running tests from the command line
    799 ##############################################################################
    800 
    801 # Note: Reuse unittest.TestProgram to launch test. In the future we may
    802 # build our own launcher to support more specific command line
    803 # parameters like test title, CSS, etc.
    804 class TestProgram(unittest.TestProgram):
    805     """
    806     A variation of the unittest.TestProgram. Please refer to the base
    807     class for command line parameters.
    808     """
    809     def runTests(self):
    810         # Pick HTMLTestRunner as the default test runner.
    811         # base class's testRunner parameter is not useful because it means
    812         # we have to instantiate HTMLTestRunner before we know self.verbosity.
    813         if self.testRunner is None:
    814             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
    815         unittest.TestProgram.runTests(self)
    816 
    817 main = TestProgram
    818 
    819 ##############################################################################
    820 # Executing this module from the command line
    821 ##############################################################################
    822 
    823 if __name__ == "__main__":
    824     main(module=None)
    HTMLTestRunner.py for Python 3.x
     1 """
      2 A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance.
      3 
      4 The simplest way to use this is to invoke its main method. E.g.
      5 
      6     import unittest
      7     import BSTestRunner
      8 
      9     ... define your tests ...
     10 
     11     if __name__ == '__main__':
     12         BSTestRunner.main()
     13 
     14 
     15 For more customization options, instantiates a BSTestRunner object.
     16 BSTestRunner is a counterpart to unittest's TextTestRunner. E.g.
     17 
     18     # output to a file
     19     fp = file('my_report.html', 'wb')
     20     runner = BSTestRunner.BSTestRunner(
     21                 stream=fp,
     22                 title='My unit test',
     23                 description='This demonstrates the report output by BSTestRunner.'
     24                 )
     25 
     26     # Use an external stylesheet.
     27     # See the Template_mixin class for more customizable options
     28     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
     29 
     30     # run the test
     31     runner.run(my_test_suite)
     32 
     33 
     34 ------------------------------------------------------------------------
     35 Copyright (c) 2004-2007, Wai Yip Tung
     36 Copyright (c) 2016, Eason Han
     37 All rights reserved.
     38 
     39 Redistribution and use in source and binary forms, with or without
     40 modification, are permitted provided that the following conditions are
     41 met:
     42 
     43 * Redistributions of source code must retain the above copyright notice,
     44   this list of conditions and the following disclaimer.
     45 * Redistributions in binary form must reproduce the above copyright
     46   notice, this list of conditions and the following disclaimer in the
     47   documentation and/or other materials provided with the distribution.
     48 * Neither the name Wai Yip Tung nor the names of its contributors may be
     49   used to endorse or promote products derived from this software without
     50   specific prior written permission.
     51 
     52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
     55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
     56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     63 """
     64 
     65 
     66 __author__ = "Wai Yip Tung && Eason Han"
     67 __version__ = "0.8.4"
     68 
     69 
     70 """
     71 Change History
     72 
     73 Version 0.8.3
     74 * Modify html style using bootstrap3.
     75 
     76 Version 0.8.3
     77 * Prevent crash on class or module-level exceptions (Darren Wurf).
     78 
     79 Version 0.8.2
     80 * Show output inline instead of popup window (Viorel Lupu).
     81 
     82 Version in 0.8.1
     83 * Validated XHTML (Wolfgang Borgert).
     84 * Added description of test classes and test cases.
     85 
     86 Version in 0.8.0
     87 * Define Template_mixin class for customization.
     88 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
     89 
     90 Version in 0.7.1
     91 * Back port to Python 2.3 (Frank Horowitz).
     92 * Fix missing scroll bars in detail log (Podi).
     93 """
     94 
     95 # TODO: color stderr
     96 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
     97 
     98 import datetime
     99 # import StringIO
    100 import io
    101 import sys
    102 import time
    103 import unittest
    104 from xml.sax import saxutils
    105 
    106 
    107 # ------------------------------------------------------------------------
    108 # The redirectors below are used to capture output during testing. Output
    109 # sent to sys.stdout and sys.stderr are automatically captured. However
    110 # in some cases sys.stdout is already cached before BSTestRunner is
    111 # invoked (e.g. calling logging.basicConfig). In order to capture those
    112 # output, use the redirectors for the cached stream.
    113 #
    114 # e.g.
    115 #   >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector)
    116 #   >>>
    117 
    118 def to_unicode(s):
    119     try:
    120         return unicode(s)
    121     except UnicodeDecodeError:
    122         # s is non ascii byte string
    123         return s.decode('unicode_escape')
    124 
    125 class OutputRedirector(object):
    126     """ Wrapper to redirect stdout or stderr """
    127     def __init__(self, fp):
    128         self.fp = fp
    129 
    130     def write(self, s):
    131         self.fp.write(to_unicode(s))
    132 
    133     def writelines(self, lines):
    134         lines = map(to_unicode, lines)
    135         self.fp.writelines(lines)
    136 
    137     def flush(self):
    138         self.fp.flush()
    139 
    140 stdout_redirector = OutputRedirector(sys.stdout)
    141 stderr_redirector = OutputRedirector(sys.stderr)
    142 
    143 
    144 
    145 # ----------------------------------------------------------------------
    146 # Template
    147 
    148 class Template_mixin(object):
    149     """
    150     Define a HTML template for report customerization and generation.
    151 
    152     Overall structure of an HTML report
    153 
    154     HTML
    155     +------------------------+
    156     |<html>                  |
    157     |  <head>                |
    158     |                        |
    159     |   STYLESHEET           |
    160     |   +----------------+   |
    161     |   |                |   |
    162     |   +----------------+   |
    163     |                        |
    164     |  </head>               |
    165     |                        |
    166     |  <body>                |
    167     |                        |
    168     |   HEADING              |
    169     |   +----------------+   |
    170     |   |                |   |
    171     |   +----------------+   |
    172     |                        |
    173     |   REPORT               |
    174     |   +----------------+   |
    175     |   |                |   |
    176     |   +----------------+   |
    177     |                        |
    178     |   ENDING               |
    179     |   +----------------+   |
    180     |   |                |   |
    181     |   +----------------+   |
    182     |                        |
    183     |  </body>               |
    184     |</html>                 |
    185     +------------------------+
    186     """
    187 
    188     STATUS = {
    189     0: 'pass',
    190     1: 'fail',
    191     2: 'error',
    192     }
    193 
    194     DEFAULT_TITLE = 'Unit Test Report'
    195     DEFAULT_DESCRIPTION = ''
    196 
    197     # ------------------------------------------------------------------------
    198     # HTML Template
    199 
    200     HTML_TMPL = r"""<!DOCTYPE html>
    201 <html lang="zh-cn">
    202   <head>
    203     <meta charset="utf-8">
    204     <meta http-equiv="X-UA-Compatible" content="IE=edge">
    205     <meta name="viewport" content="width=device-width, initial-scale=1">
    206     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    207     <title>%(title)s</title>
    208     <meta name="generator" content="%(generator)s"/>
    209     <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
    210     %(stylesheet)s
    211 
    212     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    213     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    214     <!--[if lt IE 9]>
    215       <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    216       <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
    217     <![endif]-->
    218   </head>
    219 <body>
    220 <script language="javascript" type="text/javascript"><!--
    221 output_list = Array();
    222 
    223 /* level - 0:Summary; 1:Failed; 2:All */
    224 function showCase(level) {
    225     trs = document.getElementsByTagName("tr");
    226     for (var i = 0; i < trs.length; i++) {
    227         tr = trs[i];
    228         id = tr.id;
    229         if (id.substr(0,2) == 'ft') {
    230             if (level < 1) {
    231                 tr.className = 'hiddenRow';
    232             }
    233             else {
    234                 tr.className = '';
    235             }
    236         }
    237         if (id.substr(0,2) == 'pt') {
    238             if (level > 1) {
    239                 tr.className = '';
    240             }
    241             else {
    242                 tr.className = 'hiddenRow';
    243             }
    244         }
    245     }
    246 }
    247 
    248 
    249 function showClassDetail(cid, count) {
    250     var id_list = Array(count);
    251     var toHide = 1;
    252     for (var i = 0; i < count; i++) {
    253         tid0 = 't' + cid.substr(1) + '.' + (i+1);
    254         tid = 'f' + tid0;
    255         tr = document.getElementById(tid);
    256         if (!tr) {
    257             tid = 'p' + tid0;
    258             tr = document.getElementById(tid);
    259         }
    260         id_list[i] = tid;
    261         if (tr.className) {
    262             toHide = 0;
    263         }
    264     }
    265     for (var i = 0; i < count; i++) {
    266         tid = id_list[i];
    267         if (toHide) {
    268             document.getElementById('div_'+tid).style.display = 'none'
    269             document.getElementById(tid).className = 'hiddenRow';
    270         }
    271         else {
    272             document.getElementById(tid).className = '';
    273         }
    274     }
    275 }
    276 
    277 
    278 function showTestDetail(div_id){
    279     var details_div = document.getElementById(div_id)
    280     var displayState = details_div.style.display
    281     // alert(displayState)
    282     if (displayState != 'block' ) {
    283         displayState = 'block'
    284         details_div.style.display = 'block'
    285     }
    286     else {
    287         details_div.style.display = 'none'
    288     }
    289 }
    290 
    291 
    292 function html_escape(s) {
    293     s = s.replace(/&/g,'&amp;');
    294     s = s.replace(/</g,'&lt;');
    295     s = s.replace(/>/g,'&gt;');
    296     return s;
    297 }
    298 
    299 /* obsoleted by detail in <div>
    300 function showOutput(id, name) {
    301     var w = window.open("", //url
    302                     name,
    303                     "resizable,scrollbars,status,width=800,height=450");
    304     d = w.document;
    305     d.write("<pre>");
    306     d.write(html_escape(output_list[id]));
    307     d.write("
    ");
    308     d.write("<a href='javascript:window.close()'>close</a>
    ");
    309     d.write("</pre>
    ");
    310     d.close();
    311 }
    312 */
    313 --></script>
    314 
    315 <div class="container">
    316     %(heading)s
    317     %(report)s
    318     %(ending)s
    319 </div>
    320 
    321 </body>
    322 </html>
    323 """
    324     # variables: (title, generator, stylesheet, heading, report, ending)
    325 
    326 
    327     # ------------------------------------------------------------------------
    328     # Stylesheet
    329     #
    330     # alternatively use a <link> for external style sheet, e.g.
    331     #   <link rel="stylesheet" href="$url" type="text/css">
    332 
    333     STYLESHEET_TMPL = """
    334 <style type="text/css" media="screen">
    335 
    336 /* -- css div popup ------------------------------------------------------------------------ */
    337 .popup_window {
    338     display: none;
    339     position: relative;
    340     left: 0px;
    341     top: 0px;
    342     /*border: solid #627173 1px; */
    343     padding: 10px;
    344     background-color: #99CCFF;
    345     font-family: "Lucida Console", "Courier New", Courier, monospace;
    346     text-align: left;
    347     font-size: 10pt;
    348      500px;
    349 }
    350 
    351 /* -- report ------------------------------------------------------------------------ */
    352 
    353 #show_detail_line .label {
    354     font-size: 85%;
    355     cursor: pointer;
    356 }
    357 
    358 #show_detail_line {
    359     margin: 2em auto 1em auto;
    360 }
    361 
    362 #total_row  { font-weight: bold; }
    363 .hiddenRow  { display: none; }
    364 .testcase   { margin-left: 2em; }
    365 
    366 </style>
    367 """
    368 
    369 
    370 
    371     # ------------------------------------------------------------------------
    372     # Heading
    373     #
    374 
    375     HEADING_TMPL = """<div class='heading'>
    376 <h1>%(title)s</h1>
    377 %(parameters)s
    378 <p class='description'>%(description)s</p>
    379 </div>
    380 
    381 """ # variables: (title, parameters, description)
    382 
    383     HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>
    384 """ # variables: (name, value)
    385 
    386 
    387 
    388     # ------------------------------------------------------------------------
    389     # Report
    390     #
    391 
    392     REPORT_TMPL = """
    393 <p id='show_detail_line'>
    394 <span class="label label-primary" onclick="showCase(0)">Summary</span>
    395 <span class="label label-danger" onclick="showCase(1)">Failed</span>
    396 <span class="label label-default" onclick="showCase(2)">All</span>
    397 </p>
    398 <table id='result_table' class="table">
    399     <thead>
    400         <tr id='header_row'>
    401             <th>Test Group/Test case</td>
    402             <th>Count</td>
    403             <th>Pass</td>
    404             <th>Fail</td>
    405             <th>Error</td>
    406             <th>View</td>
    407         </tr>
    408     </thead>
    409     <tbody>
    410         %(test_list)s
    411     </tbody>
    412     <tfoot>
    413         <tr id='total_row'>
    414             <td>Total</td>
    415             <td>%(count)s</td>
    416             <td class="text text-success">%(Pass)s</td>
    417             <td class="text text-danger">%(fail)s</td>
    418             <td class="text text-warning">%(error)s</td>
    419             <td>&nbsp;</td>
    420         </tr>
    421     </tfoot>
    422 </table>
    423 """ # variables: (test_list, count, Pass, fail, error)
    424 
    425     REPORT_CLASS_TMPL = r"""
    426 <tr class='%(style)s'>
    427     <td>%(desc)s</td>
    428     <td>%(count)s</td>
    429     <td>%(Pass)s</td>
    430     <td>%(fail)s</td>
    431     <td>%(error)s</td>
    432     <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
    433 </tr>
    434 """ # variables: (style, desc, count, Pass, fail, error, cid)
    435 
    436 
    437     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    438 <tr id='%(tid)s' class='%(Class)s'>
    439     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    440     <td colspan='5' align='center'>
    441 
    442     <!--css div popup start-->
    443     <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
    444         %(status)s</a>
    445 
    446     <div id='div_%(tid)s' class="popup_window">
    447         <div style='text-align: right;cursor:pointer'>
    448         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
    449            [x]</a>
    450         </div>
    451         <pre>
    452         %(script)s
    453         </pre>
    454     </div>
    455     <!--css div popup end-->
    456 
    457     </td>
    458 </tr>
    459 """ # variables: (tid, Class, style, desc, status)
    460 
    461 
    462     REPORT_TEST_NO_OUTPUT_TMPL = r"""
    463 <tr id='%(tid)s' class='%(Class)s'>
    464     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    465     <td colspan='5' align='center'>%(status)s</td>
    466 </tr>
    467 """ # variables: (tid, Class, style, desc, status)
    468 
    469 
    470     REPORT_TEST_OUTPUT_TMPL = r"""
    471 %(id)s: %(output)s
    472 """ # variables: (id, output)
    473 
    474 
    475 
    476     # ------------------------------------------------------------------------
    477     # ENDING
    478     #
    479 
    480     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    481 
    482 # -------------------- The end of the Template class -------------------
    483 
    484 
    485 TestResult = unittest.TestResult
    486 
    487 class _TestResult(TestResult):
    488     # note: _TestResult is a pure representation of results.
    489     # It lacks the output and reporting ability compares to unittest._TextTestResult.
    490 
    491     def __init__(self, verbosity=1):
    492         TestResult.__init__(self)
    493         # self.outputBuffer = StringIO.StringIO()
    494         self.outputBuffer = io.StringIO()
    495         self.stdout0 = None
    496         self.stderr0 = None
    497         self.success_count = 0
    498         self.failure_count = 0
    499         self.error_count = 0
    500         self.verbosity = verbosity
    501 
    502         # result is a list of result in 4 tuple
    503         # (
    504         #   result code (0: success; 1: fail; 2: error),
    505         #   TestCase object,
    506         #   Test output (byte string),
    507         #   stack trace,
    508         # )
    509         self.result = []
    510 
    511 
    512     def startTest(self, test):
    513         TestResult.startTest(self, test)
    514         # just one buffer for both stdout and stderr
    515         stdout_redirector.fp = self.outputBuffer
    516         stderr_redirector.fp = self.outputBuffer
    517         self.stdout0 = sys.stdout
    518         self.stderr0 = sys.stderr
    519         sys.stdout = stdout_redirector
    520         sys.stderr = stderr_redirector
    521 
    522 
    523     def complete_output(self):
    524         """
    525         Disconnect output redirection and return buffer.
    526         Safe to call multiple times.
    527         """
    528         if self.stdout0:
    529             sys.stdout = self.stdout0
    530             sys.stderr = self.stderr0
    531             self.stdout0 = None
    532             self.stderr0 = None
    533         return self.outputBuffer.getvalue()
    534 
    535 
    536     def stopTest(self, test):
    537         # Usually one of addSuccess, addError or addFailure would have been called.
    538         # But there are some path in unittest that would bypass this.
    539         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
    540         self.complete_output()
    541 
    542 
    543     def addSuccess(self, test):
    544         self.success_count += 1
    545         TestResult.addSuccess(self, test)
    546         output = self.complete_output()
    547         self.result.append((0, test, output, ''))
    548         if self.verbosity > 1:
    549             sys.stderr.write('ok ')
    550             sys.stderr.write(str(test))
    551             sys.stderr.write('
    ')
    552         else:
    553             sys.stderr.write('.')
    554 
    555     def addError(self, test, err):
    556         self.error_count += 1
    557         TestResult.addError(self, test, err)
    558         _, _exc_str = self.errors[-1]
    559         output = self.complete_output()
    560         self.result.append((2, test, output, _exc_str))
    561         if self.verbosity > 1:
    562             sys.stderr.write('E  ')
    563             sys.stderr.write(str(test))
    564             sys.stderr.write('
    ')
    565         else:
    566             sys.stderr.write('E')
    567 
    568     def addFailure(self, test, err):
    569         self.failure_count += 1
    570         TestResult.addFailure(self, test, err)
    571         _, _exc_str = self.failures[-1]
    572         output = self.complete_output()
    573         self.result.append((1, test, output, _exc_str))
    574         if self.verbosity > 1:
    575             sys.stderr.write('F  ')
    576             sys.stderr.write(str(test))
    577             sys.stderr.write('
    ')
    578         else:
    579             sys.stderr.write('F')
    580 
    581 
    582 class BSTestRunner(Template_mixin):
    583     """
    584     """
    585     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
    586         self.stream = stream
    587         self.verbosity = verbosity
    588         if title is None:
    589             self.title = self.DEFAULT_TITLE
    590         else:
    591             self.title = title
    592         if description is None:
    593             self.description = self.DEFAULT_DESCRIPTION
    594         else:
    595             self.description = description
    596 
    597         self.startTime = datetime.datetime.now()
    598 
    599 
    600     def run(self, test):
    601         "Run the given test case or test suite."
    602         result = _TestResult(self.verbosity)
    603         test(result)
    604         self.stopTime = datetime.datetime.now()
    605         self.generateReport(test, result)
    606         # print >>sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime)
    607         print(sys.stderr, '
    Time Elapsed: %s' % (self.stopTime - self.startTime))
    608         return result
    609 
    610 
    611     def sortResult(self, result_list):
    612         # unittest does not seems to run in any particular order.
    613         # Here at least we want to group them together by class.
    614         rmap = {}
    615         classes = []
    616         for n,t,o,e in result_list:
    617             cls = t.__class__
    618             # if not rmap.has_key(cls):
    619             if not cls in rmap:
    620                 rmap[cls] = []
    621                 classes.append(cls)
    622             rmap[cls].append((n,t,o,e))
    623         r = [(cls, rmap[cls]) for cls in classes]
    624         return r
    625 
    626 
    627     def getReportAttributes(self, result):
    628         """
    629         Return report attributes as a list of (name, value).
    630         Override this to add custom attributes.
    631         """
    632         startTime = str(self.startTime)[:19]
    633         duration = str(self.stopTime - self.startTime)
    634         status = []
    635         if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>'    % result.success_count)
    636         if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
    637         if result.error_count:   status.append('<span class="text text-warning">Error <strong>%s</strong></span>'   % result.error_count  )
    638         if status:
    639             status = ' '.join(status)
    640         else:
    641             status = 'none'
    642         return [
    643             ('Start Time', startTime),
    644             ('Duration', duration),
    645             ('Status', status),
    646         ]
    647 
    648 
    649     def generateReport(self, test, result):
    650         report_attrs = self.getReportAttributes(result)
    651         generator = 'BSTestRunner %s' % __version__
    652         stylesheet = self._generate_stylesheet()
    653         heading = self._generate_heading(report_attrs)
    654         report = self._generate_report(result)
    655         ending = self._generate_ending()
    656         output = self.HTML_TMPL % dict(
    657             title = saxutils.escape(self.title),
    658             generator = generator,
    659             stylesheet = stylesheet,
    660             heading = heading,
    661             report = report,
    662             ending = ending,
    663         )
    664         self.stream.write(output.encode('utf8'))
    665 
    666 
    667     def _generate_stylesheet(self):
    668         return self.STYLESHEET_TMPL
    669 
    670 
    671     def _generate_heading(self, report_attrs):
    672         a_lines = []
    673         for name, value in report_attrs:
    674             line = self.HEADING_ATTRIBUTE_TMPL % dict(
    675                     # name = saxutils.escape(name),
    676                     # value = saxutils.escape(value),
    677                     name = name,
    678                     value = value,
    679                 )
    680             a_lines.append(line)
    681         heading = self.HEADING_TMPL % dict(
    682             title = saxutils.escape(self.title),
    683             parameters = ''.join(a_lines),
    684             description = saxutils.escape(self.description),
    685         )
    686         return heading
    687 
    688 
    689     def _generate_report(self, result):
    690         rows = []
    691         sortedResult = self.sortResult(result.result)
    692         for cid, (cls, cls_results) in enumerate(sortedResult):
    693             # subtotal for a class
    694             np = nf = ne = 0
    695             for n,t,o,e in cls_results:
    696                 if n == 0: np += 1
    697                 elif n == 1: nf += 1
    698                 else: ne += 1
    699 
    700             # format class description
    701             if cls.__module__ == "__main__":
    702                 name = cls.__name__
    703             else:
    704                 name = "%s.%s" % (cls.__module__, cls.__name__)
    705             doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
    706             desc = doc and '%s: %s' % (name, doc) or name
    707 
    708             row = self.REPORT_CLASS_TMPL % dict(
    709                 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
    710                 desc = desc,
    711                 count = np+nf+ne,
    712                 Pass = np,
    713                 fail = nf,
    714                 error = ne,
    715                 cid = 'c%s' % (cid+1),
    716             )
    717             rows.append(row)
    718 
    719             for tid, (n,t,o,e) in enumerate(cls_results):
    720                 self._generate_report_test(rows, cid, tid, n, t, o, e)
    721 
    722         report = self.REPORT_TMPL % dict(
    723             test_list = ''.join(rows),
    724             count = str(result.success_count+result.failure_count+result.error_count),
    725             Pass = str(result.success_count),
    726             fail = str(result.failure_count),
    727             error = str(result.error_count),
    728         )
    729         return report
    730 
    731 
    732     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
    733         # e.g. 'pt1.1', 'ft1.1', etc
    734         has_output = bool(o or e)
    735         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
    736         name = t.id().split('.')[-1]
    737         doc = t.shortDescription() or ""
    738         desc = doc and ('%s: %s' % (name, doc)) or name
    739         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    740 
    741         # o and e should be byte string because they are collected from stdout and stderr?
    742         if isinstance(o,str):
    743             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    744             # uo = unicode(o.encode('string_escape'))
    745             # uo = o.decode('latin-1')
    746             uo = o
    747         else:
    748             uo = o
    749         if isinstance(e,str):
    750             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    751             # ue = unicode(e.encode('string_escape'))
    752             # ue = e.decode('latin-1')
    753             ue=e
    754         else:
    755             ue = e
    756 
    757         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
    758             id = tid,
    759             output = saxutils.escape(uo+ue),
    760         )
    761 
    762         row = tmpl % dict(
    763             tid = tid,
    764             # Class = (n == 0 and 'hiddenRow' or 'none'),
    765             Class = (n == 0 and 'hiddenRow' or 'text text-success'),
    766             # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
    767             style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
    768             desc = desc,
    769             script = script,
    770             status = self.STATUS[n],
    771         )
    772         rows.append(row)
    773         if not has_output:
    774             return
    775 
    776     def _generate_ending(self):
    777         return self.ENDING_TMPL
    778 
    779 
    780 ##############################################################################
    781 # Facilities for running tests from the command line
    782 ##############################################################################
    783 
    784 # Note: Reuse unittest.TestProgram to launch test. In the future we may
    785 # build our own launcher to support more specific command line
    786 # parameters like test title, CSS, etc.
    787 class TestProgram(unittest.TestProgram):
    788     """
    789     A variation of the unittest.TestProgram. Please refer to the base
    790     class for command line parameters.
    791     """
    792     def runTests(self):
    793         # Pick BSTestRunner as the default test runner.
    794         # base class's testRunner parameter is not useful because it means
    795         # we have to instantiate BSTestRunner before we know self.verbosity.
    796         if self.testRunner is None:
    797             self.testRunner = BSTestRunner(verbosity=self.verbosity)
    798         unittest.TestProgram.runTests(self)
    799 
    800 main = TestProgram
    801 
    802 ##############################################################################
    803 # Executing this module from the command line
    804 ##############################################################################
    805 
    806 if __name__ == "__main__":
    807     main(module=None)
    BSTestRunner.py for Python 3.x
      1 """
      2 A TestRunner for use with the Python unit testing framework. It
      3 generates a HTML report to show the result at a glance.
      4 
      5 The simplest way to use this is to invoke its main method. E.g.
      6 
      7     import unittest
      8     import HTMLTestRunner
      9 
     10     ... define your tests ...
     11 
     12     if __name__ == '__main__':
     13         HTMLTestRunner.main()
     14 
     15 
     16 For more customization options, instantiates a HTMLTestRunner object.
     17 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
     18 
     19     # output to a file
     20     fp = file('my_report.html', 'wb')
     21     runner = HTMLTestRunner.HTMLTestRunner(
     22                 stream=fp,
     23                 title='My unit test',
     24                 description='This demonstrates the report output by HTMLTestRunner.'
     25                 )
     26 
     27     # Use an external stylesheet.
     28     # See the Template_mixin class for more customizable options
     29     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
     30 
     31     # run the test
     32     runner.run(my_test_suite)
     33 
     34 
     35 ------------------------------------------------------------------------
     36 Copyright (c) 2004-2007, Wai Yip Tung
     37 All rights reserved.
     38 
     39 Redistribution and use in source and binary forms, with or without
     40 modification, are permitted provided that the following conditions are
     41 met:
     42 
     43 * Redistributions of source code must retain the above copyright notice,
     44   this list of conditions and the following disclaimer.
     45 * Redistributions in binary form must reproduce the above copyright
     46   notice, this list of conditions and the following disclaimer in the
     47   documentation and/or other materials provided with the distribution.
     48 * Neither the name Wai Yip Tung nor the names of its contributors may be
     49   used to endorse or promote products derived from this software without
     50   specific prior written permission.
     51 
     52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
     55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
     56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     63 """
     64 
     65 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
     66 
     67 __author__ = "Wai Yip Tung"
     68 __version__ = "0.8.2"
     69 
     70 
     71 """
     72 Change History
     73 
     74 Version 0.8.2
     75 * Show output inline instead of popup window (Viorel Lupu).
     76 
     77 Version in 0.8.1
     78 * Validated XHTML (Wolfgang Borgert).
     79 * Added description of test classes and test cases.
     80 
     81 Version in 0.8.0
     82 * Define Template_mixin class for customization.
     83 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
     84 
     85 Version in 0.7.1
     86 * Back port to Python 2.3 (Frank Horowitz).
     87 * Fix missing scroll bars in detail log (Podi).
     88 """
     89 
     90 # TODO: color stderr
     91 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
     92 
     93 import datetime
     94 import StringIO
     95 import sys
     96 import time
     97 import unittest
     98 from xml.sax import saxutils
     99 
    100 
    101 # ------------------------------------------------------------------------
    102 # The redirectors below are used to capture output during testing. Output
    103 # sent to sys.stdout and sys.stderr are automatically captured. However
    104 # in some cases sys.stdout is already cached before HTMLTestRunner is
    105 # invoked (e.g. calling logging.basicConfig). In order to capture those
    106 # output, use the redirectors for the cached stream.
    107 #
    108 # e.g.
    109 #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
    110 #   >>>
    111 
    112 class OutputRedirector(object):
    113     """ Wrapper to redirect stdout or stderr """
    114     def __init__(self, fp):
    115         self.fp = fp
    116 
    117     def write(self, s):
    118         self.fp.write(s)
    119 
    120     def writelines(self, lines):
    121         self.fp.writelines(lines)
    122 
    123     def flush(self):
    124         self.fp.flush()
    125 
    126 stdout_redirector = OutputRedirector(sys.stdout)
    127 stderr_redirector = OutputRedirector(sys.stderr)
    128 
    129 
    130 
    131 # ----------------------------------------------------------------------
    132 # Template
    133 
    134 class Template_mixin(object):
    135     """
    136     Define a HTML template for report customerization and generation.
    137 
    138     Overall structure of an HTML report
    139 
    140     HTML
    141     +------------------------+
    142     |<html>                  |
    143     |  <head>                |
    144     |                        |
    145     |   STYLESHEET           |
    146     |   +----------------+   |
    147     |   |                |   |
    148     |   +----------------+   |
    149     |                        |
    150     |  </head>               |
    151     |                        |
    152     |  <body>                |
    153     |                        |
    154     |   HEADING              |
    155     |   +----------------+   |
    156     |   |                |   |
    157     |   +----------------+   |
    158     |                        |
    159     |   REPORT               |
    160     |   +----------------+   |
    161     |   |                |   |
    162     |   +----------------+   |
    163     |                        |
    164     |   ENDING               |
    165     |   +----------------+   |
    166     |   |                |   |
    167     |   +----------------+   |
    168     |                        |
    169     |  </body>               |
    170     |</html>                 |
    171     +------------------------+
    172     """
    173 
    174     STATUS = {
    175     0: 'pass',
    176     1: 'fail',
    177     2: 'error',
    178     }
    179 
    180     DEFAULT_TITLE = 'Unit Test Report'
    181     DEFAULT_DESCRIPTION = ''
    182 
    183     # ------------------------------------------------------------------------
    184     # HTML Template
    185 
    186     HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
    187 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    188 <html xmlns="http://www.w3.org/1999/xhtml">
    189 <head>
    190     <title>%(title)s</title>
    191     <meta name="generator" content="%(generator)s"/>
    192     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    193     %(stylesheet)s
    194 </head>
    195 <body>
    196 <script language="javascript" type="text/javascript"><!--
    197 output_list = Array();
    198 
    199 /* level - 0:Summary; 1:Failed; 2:All */
    200 function showCase(level) {
    201     trs = document.getElementsByTagName("tr");
    202     for (var i = 0; i < trs.length; i++) {
    203         tr = trs[i];
    204         id = tr.id;
    205         if (id.substr(0,2) == 'ft') {
    206             if (level < 1) {
    207                 tr.className = 'hiddenRow';
    208             }
    209             else {
    210                 tr.className = '';
    211             }
    212         }
    213         if (id.substr(0,2) == 'pt') {
    214             if (level > 1) {
    215                 tr.className = '';
    216             }
    217             else {
    218                 tr.className = 'hiddenRow';
    219             }
    220         }
    221     }
    222 }
    223 
    224 
    225 function showClassDetail(cid, count) {
    226     var id_list = Array(count);
    227     var toHide = 1;
    228     for (var i = 0; i < count; i++) {
    229         tid0 = 't' + cid.substr(1) + '.' + (i+1);
    230         tid = 'f' + tid0;
    231         tr = document.getElementById(tid);
    232         if (!tr) {
    233             tid = 'p' + tid0;
    234             tr = document.getElementById(tid);
    235         }
    236         id_list[i] = tid;
    237         if (tr.className) {
    238             toHide = 0;
    239         }
    240     }
    241     for (var i = 0; i < count; i++) {
    242         tid = id_list[i];
    243         if (toHide) {
    244             document.getElementById('div_'+tid).style.display = 'none'
    245             document.getElementById(tid).className = 'hiddenRow';
    246         }
    247         else {
    248             document.getElementById(tid).className = '';
    249         }
    250     }
    251 }
    252 
    253 
    254 function showTestDetail(div_id){
    255     var details_div = document.getElementById(div_id)
    256     var displayState = details_div.style.display
    257     // alert(displayState)
    258     if (displayState != 'block' ) {
    259         displayState = 'block'
    260         details_div.style.display = 'block'
    261     }
    262     else {
    263         details_div.style.display = 'none'
    264     }
    265 }
    266 
    267 
    268 function html_escape(s) {
    269     s = s.replace(/&/g,'&amp;');
    270     s = s.replace(/</g,'&lt;');
    271     s = s.replace(/>/g,'&gt;');
    272     return s;
    273 }
    274 
    275 /* obsoleted by detail in <div>
    276 function showOutput(id, name) {
    277     var w = window.open("", //url
    278                     name,
    279                     "resizable,scrollbars,status,width=800,height=450");
    280     d = w.document;
    281     d.write("<pre>");
    282     d.write(html_escape(output_list[id]));
    283     d.write("
    ");
    284     d.write("<a href='javascript:window.close()'>close</a>
    ");
    285     d.write("</pre>
    ");
    286     d.close();
    287 }
    288 */
    289 --></script>
    290 
    291 %(heading)s
    292 %(report)s
    293 %(ending)s
    294 
    295 </body>
    296 </html>
    297 """
    298     # variables: (title, generator, stylesheet, heading, report, ending)
    299 
    300 
    301     # ------------------------------------------------------------------------
    302     # Stylesheet
    303     #
    304     # alternatively use a <link> for external style sheet, e.g.
    305     #   <link rel="stylesheet" href="$url" type="text/css">
    306 
    307     STYLESHEET_TMPL = """
    308 <style type="text/css" media="screen">
    309 body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
    310 table       { font-size: 100%; }
    311 pre         { }
    312 
    313 /* -- heading ---------------------------------------------------------------------- */
    314 h1 {
    315     font-size: 16pt;
    316     color: gray;
    317 }
    318 .heading {
    319     margin-top: 0ex;
    320     margin-bottom: 1ex;
    321 }
    322 
    323 .heading .attribute {
    324     margin-top: 1ex;
    325     margin-bottom: 0;
    326 }
    327 
    328 .heading .description {
    329     margin-top: 4ex;
    330     margin-bottom: 6ex;
    331 }
    332 
    333 /* -- css div popup ------------------------------------------------------------------------ */
    334 a.popup_link {
    335 }
    336 
    337 a.popup_link:hover {
    338     color: red;
    339 }
    340 
    341 .popup_window {
    342     display: none;
    343     position: relative;
    344     left: 0px;
    345     top: 0px;
    346     /*border: solid #627173 1px; */
    347     padding: 10px;
    348     background-color: #E6E6D6;
    349     font-family: "Lucida Console", "Courier New", Courier, monospace;
    350     text-align: left;
    351     font-size: 8pt;
    352      500px;
    353 }
    354 
    355 }
    356 /* -- report ------------------------------------------------------------------------ */
    357 #show_detail_line {
    358     margin-top: 3ex;
    359     margin-bottom: 1ex;
    360 }
    361 #result_table {
    362      80%;
    363     border-collapse: collapse;
    364     border: 1px solid #777;
    365 }
    366 #header_row {
    367     font-weight: bold;
    368     color: white;
    369     background-color: #777;
    370 }
    371 #result_table td {
    372     border: 1px solid #777;
    373     padding: 2px;
    374 }
    375 #total_row  { font-weight: bold; }
    376 .passClass  { background-color: #6c6; }
    377 .failClass  { background-color: #c60; }
    378 .errorClass { background-color: #c00; }
    379 .passCase   { color: #6c6; }
    380 .failCase   { color: #c60; font-weight: bold; }
    381 .errorCase  { color: #c00; font-weight: bold; }
    382 .hiddenRow  { display: none; }
    383 .testcase   { margin-left: 2em; }
    384 
    385 
    386 /* -- ending ---------------------------------------------------------------------- */
    387 #ending {
    388 }
    389 
    390 </style>
    391 """
    392 
    393 
    394 
    395     # ------------------------------------------------------------------------
    396     # Heading
    397     #
    398 
    399     HEADING_TMPL = """<div class='heading'>
    400 <h1>%(title)s</h1>
    401 %(parameters)s
    402 <p class='description'>%(description)s</p>
    403 </div>
    404 
    405 """ # variables: (title, parameters, description)
    406 
    407     HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
    408 """ # variables: (name, value)
    409 
    410 
    411 
    412     # ------------------------------------------------------------------------
    413     # Report
    414     #
    415 
    416     REPORT_TMPL = """
    417 <p id='show_detail_line'>Show
    418 <a href='javascript:showCase(0)'>Summary</a>
    419 <a href='javascript:showCase(1)'>Failed</a>
    420 <a href='javascript:showCase(2)'>All</a>
    421 </p>
    422 <table id='result_table'>
    423 <colgroup>
    424 <col align='left' />
    425 <col align='right' />
    426 <col align='right' />
    427 <col align='right' />
    428 <col align='right' />
    429 <col align='right' />
    430 </colgroup>
    431 <tr id='header_row'>
    432     <td>Test Group/Test case</td>
    433     <td>Count</td>
    434     <td>Pass</td>
    435     <td>Fail</td>
    436     <td>Error</td>
    437     <td>View</td>
    438 </tr>
    439 %(test_list)s
    440 <tr id='total_row'>
    441     <td>Total</td>
    442     <td>%(count)s</td>
    443     <td>%(Pass)s</td>
    444     <td>%(fail)s</td>
    445     <td>%(error)s</td>
    446     <td>&nbsp;</td>
    447 </tr>
    448 </table>
    449 """ # variables: (test_list, count, Pass, fail, error)
    450 
    451     REPORT_CLASS_TMPL = r"""
    452 <tr class='%(style)s'>
    453     <td>%(desc)s</td>
    454     <td>%(count)s</td>
    455     <td>%(Pass)s</td>
    456     <td>%(fail)s</td>
    457     <td>%(error)s</td>
    458     <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
    459 </tr>
    460 """ # variables: (style, desc, count, Pass, fail, error, cid)
    461 
    462 
    463     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    464 <tr id='%(tid)s' class='%(Class)s'>
    465     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    466     <td colspan='5' align='center'>
    467 
    468     <!--css div popup start-->
    469     <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
    470         %(status)s</a>
    471 
    472     <div id='div_%(tid)s' class="popup_window">
    473         <div style='text-align: right; color:red;cursor:pointer'>
    474         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
    475            [x]</a>
    476         </div>
    477         <pre>
    478         %(script)s
    479         </pre>
    480     </div>
    481     <!--css div popup end-->
    482 
    483     </td>
    484 </tr>
    485 """ # variables: (tid, Class, style, desc, status)
    486 
    487 
    488     REPORT_TEST_NO_OUTPUT_TMPL = r"""
    489 <tr id='%(tid)s' class='%(Class)s'>
    490     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    491     <td colspan='5' align='center'>%(status)s</td>
    492 </tr>
    493 """ # variables: (tid, Class, style, desc, status)
    494 
    495 
    496     REPORT_TEST_OUTPUT_TMPL = r"""
    497 %(id)s: %(output)s
    498 """ # variables: (id, output)
    499 
    500 
    501 
    502     # ------------------------------------------------------------------------
    503     # ENDING
    504     #
    505 
    506     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    507 
    508 # -------------------- The end of the Template class -------------------
    509 
    510 
    511 TestResult = unittest.TestResult
    512 
    513 class _TestResult(TestResult):
    514     # note: _TestResult is a pure representation of results.
    515     # It lacks the output and reporting ability compares to unittest._TextTestResult.
    516 
    517     def __init__(self, verbosity=1):
    518         TestResult.__init__(self)
    519         self.stdout0 = None
    520         self.stderr0 = None
    521         self.success_count = 0
    522         self.failure_count = 0
    523         self.error_count = 0
    524         self.verbosity = verbosity
    525 
    526         # result is a list of result in 4 tuple
    527         # (
    528         #   result code (0: success; 1: fail; 2: error),
    529         #   TestCase object,
    530         #   Test output (byte string),
    531         #   stack trace,
    532         # )
    533         self.result = []
    534 
    535 
    536     def startTest(self, test):
    537         TestResult.startTest(self, test)
    538         # just one buffer for both stdout and stderr
    539         self.outputBuffer = StringIO.StringIO()
    540         stdout_redirector.fp = self.outputBuffer
    541         stderr_redirector.fp = self.outputBuffer
    542         self.stdout0 = sys.stdout
    543         self.stderr0 = sys.stderr
    544         sys.stdout = stdout_redirector
    545         sys.stderr = stderr_redirector
    546 
    547 
    548     def complete_output(self):
    549         """
    550         Disconnect output redirection and return buffer.
    551         Safe to call multiple times.
    552         """
    553         if self.stdout0:
    554             sys.stdout = self.stdout0
    555             sys.stderr = self.stderr0
    556             self.stdout0 = None
    557             self.stderr0 = None
    558         return self.outputBuffer.getvalue()
    559 
    560 
    561     def stopTest(self, test):
    562         # Usually one of addSuccess, addError or addFailure would have been called.
    563         # But there are some path in unittest that would bypass this.
    564         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
    565         self.complete_output()
    566 
    567 
    568     def addSuccess(self, test):
    569         self.success_count += 1
    570         TestResult.addSuccess(self, test)
    571         output = self.complete_output()
    572         self.result.append((0, test, output, ''))
    573         if self.verbosity > 1:
    574             sys.stderr.write('ok ')
    575             sys.stderr.write(str(test))
    576             sys.stderr.write('
    ')
    577         else:
    578             sys.stderr.write('.')
    579 
    580     def addError(self, test, err):
    581         self.error_count += 1
    582         TestResult.addError(self, test, err)
    583         _, _exc_str = self.errors[-1]
    584         output = self.complete_output()
    585         self.result.append((2, test, output, _exc_str))
    586         if self.verbosity > 1:
    587             sys.stderr.write('E  ')
    588             sys.stderr.write(str(test))
    589             sys.stderr.write('
    ')
    590         else:
    591             sys.stderr.write('E')
    592 
    593     def addFailure(self, test, err):
    594         self.failure_count += 1
    595         TestResult.addFailure(self, test, err)
    596         _, _exc_str = self.failures[-1]
    597         output = self.complete_output()
    598         self.result.append((1, test, output, _exc_str))
    599         if self.verbosity > 1:
    600             sys.stderr.write('F  ')
    601             sys.stderr.write(str(test))
    602             sys.stderr.write('
    ')
    603         else:
    604             sys.stderr.write('F')
    605 
    606 
    607 class HTMLTestRunner(Template_mixin):
    608     """
    609     """
    610     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
    611         self.stream = stream
    612         self.verbosity = verbosity
    613         if title is None:
    614             self.title = self.DEFAULT_TITLE
    615         else:
    616             self.title = title
    617         if description is None:
    618             self.description = self.DEFAULT_DESCRIPTION
    619         else:
    620             self.description = description
    621 
    622         self.startTime = datetime.datetime.now()
    623 
    624 
    625     def run(self, test):
    626         "Run the given test case or test suite."
    627         result = _TestResult(self.verbosity)
    628         test(result)
    629         self.stopTime = datetime.datetime.now()
    630         self.generateReport(test, result)
    631         print >>sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime)
    632         return result
    633 
    634 
    635     def sortResult(self, result_list):
    636         # unittest does not seems to run in any particular order.
    637         # Here at least we want to group them together by class.
    638         rmap = {}
    639         classes = []
    640         for n,t,o,e in result_list:
    641             cls = t.__class__
    642             if not rmap.has_key(cls):
    643                 rmap[cls] = []
    644                 classes.append(cls)
    645             rmap[cls].append((n,t,o,e))
    646         r = [(cls, rmap[cls]) for cls in classes]
    647         return r
    648 
    649 
    650     def getReportAttributes(self, result):
    651         """
    652         Return report attributes as a list of (name, value).
    653         Override this to add custom attributes.
    654         """
    655         startTime = str(self.startTime)[:19]
    656         duration = str(self.stopTime - self.startTime)
    657         status = []
    658         if result.success_count: status.append('Pass %s'    % result.success_count)
    659         if result.failure_count: status.append('Failure %s' % result.failure_count)
    660         if result.error_count:   status.append('Error %s'   % result.error_count  )
    661         if status:
    662             status = ' '.join(status)
    663         else:
    664             status = 'none'
    665         return [
    666             ('Start Time', startTime),
    667             ('Duration', duration),
    668             ('Status', status),
    669         ]
    670 
    671 
    672     def generateReport(self, test, result):
    673         report_attrs = self.getReportAttributes(result)
    674         generator = 'HTMLTestRunner %s' % __version__
    675         stylesheet = self._generate_stylesheet()
    676         heading = self._generate_heading(report_attrs)
    677         report = self._generate_report(result)
    678         ending = self._generate_ending()
    679         output = self.HTML_TMPL % dict(
    680             title = saxutils.escape(self.title),
    681             generator = generator,
    682             stylesheet = stylesheet,
    683             heading = heading,
    684             report = report,
    685             ending = ending,
    686         )
    687         self.stream.write(output.encode('utf8'))
    688 
    689 
    690     def _generate_stylesheet(self):
    691         return self.STYLESHEET_TMPL
    692 
    693 
    694     def _generate_heading(self, report_attrs):
    695         a_lines = []
    696         for name, value in report_attrs:
    697             line = self.HEADING_ATTRIBUTE_TMPL % dict(
    698                     name = saxutils.escape(name),
    699                     value = saxutils.escape(value),
    700                 )
    701             a_lines.append(line)
    702         heading = self.HEADING_TMPL % dict(
    703             title = saxutils.escape(self.title),
    704             parameters = ''.join(a_lines),
    705             description = saxutils.escape(self.description),
    706         )
    707         return heading
    708 
    709 
    710     def _generate_report(self, result):
    711         rows = []
    712         sortedResult = self.sortResult(result.result)
    713         for cid, (cls, cls_results) in enumerate(sortedResult):
    714             # subtotal for a class
    715             np = nf = ne = 0
    716             for n,t,o,e in cls_results:
    717                 if n == 0: np += 1
    718                 elif n == 1: nf += 1
    719                 else: ne += 1
    720 
    721             # format class description
    722             if cls.__module__ == "__main__":
    723                 name = cls.__name__
    724             else:
    725                 name = "%s.%s" % (cls.__module__, cls.__name__)
    726             doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
    727             desc = doc and '%s: %s' % (name, doc) or name
    728 
    729             row = self.REPORT_CLASS_TMPL % dict(
    730                 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
    731                 desc = desc,
    732                 count = np+nf+ne,
    733                 Pass = np,
    734                 fail = nf,
    735                 error = ne,
    736                 cid = 'c%s' % (cid+1),
    737             )
    738             rows.append(row)
    739 
    740             for tid, (n,t,o,e) in enumerate(cls_results):
    741                 self._generate_report_test(rows, cid, tid, n, t, o, e)
    742 
    743         report = self.REPORT_TMPL % dict(
    744             test_list = ''.join(rows),
    745             count = str(result.success_count+result.failure_count+result.error_count),
    746             Pass = str(result.success_count),
    747             fail = str(result.failure_count),
    748             error = str(result.error_count),
    749         )
    750         return report
    751 
    752 
    753     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
    754         # e.g. 'pt1.1', 'ft1.1', etc
    755         has_output = bool(o or e)
    756         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
    757         name = t.id().split('.')[-1]
    758         doc = t.shortDescription() or ""
    759         desc = doc and ('%s: %s' % (name, doc)) or name
    760         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    761 
    762         # o and e should be byte string because they are collected from stdout and stderr?
    763         if isinstance(o,str):
    764             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    765             # uo = unicode(o.encode('string_escape'))
    766             uo = o.decode('latin-1')
    767         else:
    768             uo = o
    769         if isinstance(e,str):
    770             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    771             # ue = unicode(e.encode('string_escape'))
    772             ue = e.decode('latin-1')
    773         else:
    774             ue = e
    775 
    776         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
    777             id = tid,
    778             output = saxutils.escape(uo+ue),
    779         )
    780 
    781         row = tmpl % dict(
    782             tid = tid,
    783             Class = (n == 0 and 'hiddenRow' or 'none'),
    784             style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
    785             desc = desc,
    786             script = script,
    787             status = self.STATUS[n],
    788         )
    789         rows.append(row)
    790         if not has_output:
    791             return
    792 
    793     def _generate_ending(self):
    794         return self.ENDING_TMPL
    795 
    796 
    797 ##############################################################################
    798 # Facilities for running tests from the command line
    799 ##############################################################################
    800 
    801 # Note: Reuse unittest.TestProgram to launch test. In the future we may
    802 # build our own launcher to support more specific command line
    803 # parameters like test title, CSS, etc.
    804 class TestProgram(unittest.TestProgram):
    805     """
    806     A variation of the unittest.TestProgram. Please refer to the base
    807     class for command line parameters.
    808     """
    809     def runTests(self):
    810         # Pick HTMLTestRunner as the default test runner.
    811         # base class's testRunner parameter is not useful because it means
    812         # we have to instantiate HTMLTestRunner before we know self.verbosity.
    813         if self.testRunner is None:
    814             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
    815         unittest.TestProgram.runTests(self)
    816 
    817 main = TestProgram
    818 
    819 ##############################################################################
    820 # Executing this module from the command line
    821 ##############################################################################
    822 
    823 if __name__ == "__main__":
    824     main(module=None)
    HTMLTestRunner.py for Python 2.x
      1 """
      2 A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance.
      3 The simplest way to use this is to invoke its main method. E.g.
      4     import unittest
      5     import BSTestRunner
      6     ... define your tests ...
      7     if __name__ == '__main__':
      8         BSTestRunner.main()
      9 For more customization options, instantiates a BSTestRunner object.
     10 BSTestRunner is a counterpart to unittest's TextTestRunner. E.g.
     11     # output to a file
     12     fp = file('my_report.html', 'wb')
     13     runner = BSTestRunner.BSTestRunner(
     14                 stream=fp,
     15                 title='My unit test',
     16                 description='This demonstrates the report output by BSTestRunner.'
     17                 )
     18     # Use an external stylesheet.
     19     # See the Template_mixin class for more customizable options
     20     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
     21     # run the test
     22     runner.run(my_test_suite)
     23 ------------------------------------------------------------------------
     24 Copyright (c) 2004-2007, Wai Yip Tung
     25 Copyright (c) 2016, Eason Han
     26 All rights reserved.
     27 Redistribution and use in source and binary forms, with or without
     28 modification, are permitted provided that the following conditions are
     29 met:
     30 * Redistributions of source code must retain the above copyright notice,
     31   this list of conditions and the following disclaimer.
     32 * Redistributions in binary form must reproduce the above copyright
     33   notice, this list of conditions and the following disclaimer in the
     34   documentation and/or other materials provided with the distribution.
     35 * Neither the name Wai Yip Tung nor the names of its contributors may be
     36   used to endorse or promote products derived from this software without
     37   specific prior written permission.
     38 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     39 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     40 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
     41 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
     42 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     43 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     44 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     45 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     46 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     47 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     48 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     49 """
     50 
     51 
     52 __author__ = "Wai Yip Tung && Eason Han"
     53 __version__ = "0.8.4"
     54 
     55 
     56 """
     57 Change History
     58 Version 0.8.3
     59 * Modify html style using bootstrap3.
     60 Version 0.8.3
     61 * Prevent crash on class or module-level exceptions (Darren Wurf).
     62 Version 0.8.2
     63 * Show output inline instead of popup window (Viorel Lupu).
     64 Version in 0.8.1
     65 * Validated XHTML (Wolfgang Borgert).
     66 * Added description of test classes and test cases.
     67 Version in 0.8.0
     68 * Define Template_mixin class for customization.
     69 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
     70 Version in 0.7.1
     71 * Back port to Python 2.3 (Frank Horowitz).
     72 * Fix missing scroll bars in detail log (Podi).
     73 """
     74 
     75 # TODO: color stderr
     76 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
     77 
     78 import datetime
     79 try:
     80     from StringIO import StringIO
     81 except ImportError:
     82     from io import StringIO
     83 import sys
     84 import time
     85 import unittest
     86 from xml.sax import saxutils
     87 
     88 
     89 # ------------------------------------------------------------------------
     90 # The redirectors below are used to capture output during testing. Output
     91 # sent to sys.stdout and sys.stderr are automatically captured. However
     92 # in some cases sys.stdout is already cached before BSTestRunner is
     93 # invoked (e.g. calling logging.basicConfig). In order to capture those
     94 # output, use the redirectors for the cached stream.
     95 #
     96 # e.g.
     97 #   >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector)
     98 #   >>>
     99 
    100 def to_unicode(s):
    101     try:
    102         return unicode(s)
    103     except UnicodeDecodeError:
    104         # s is non ascii byte string
    105         return s.decode('unicode_escape')
    106 
    107 class OutputRedirector(object):
    108     """ Wrapper to redirect stdout or stderr """
    109     def __init__(self, fp):
    110         self.fp = fp
    111 
    112     def write(self, s):
    113         self.fp.write(to_unicode(s))
    114 
    115     def writelines(self, lines):
    116         lines = map(to_unicode, lines)
    117         self.fp.writelines(lines)
    118 
    119     def flush(self):
    120         self.fp.flush()
    121 
    122 stdout_redirector = OutputRedirector(sys.stdout)
    123 stderr_redirector = OutputRedirector(sys.stderr)
    124 
    125 
    126 
    127 # ----------------------------------------------------------------------
    128 # Template
    129 
    130 class Template_mixin(object):
    131     """
    132     Define a HTML template for report customerization and generation.
    133     Overall structure of an HTML report
    134     HTML
    135     +------------------------+
    136     |<html>                  |
    137     |  <head>                |
    138     |                        |
    139     |   STYLESHEET           |
    140     |   +----------------+   |
    141     |   |                |   |
    142     |   +----------------+   |
    143     |                        |
    144     |  </head>               |
    145     |                        |
    146     |  <body>                |
    147     |                        |
    148     |   HEADING              |
    149     |   +----------------+   |
    150     |   |                |   |
    151     |   +----------------+   |
    152     |                        |
    153     |   REPORT               |
    154     |   +----------------+   |
    155     |   |                |   |
    156     |   +----------------+   |
    157     |                        |
    158     |   ENDING               |
    159     |   +----------------+   |
    160     |   |                |   |
    161     |   +----------------+   |
    162     |                        |
    163     |  </body>               |
    164     |</html>                 |
    165     +------------------------+
    166     """
    167 
    168     STATUS = {
    169     0: 'pass',
    170     1: 'fail',
    171     2: 'error',
    172     }
    173 
    174     DEFAULT_TITLE = 'Unit Test Report'
    175     DEFAULT_DESCRIPTION = ''
    176 
    177     # ------------------------------------------------------------------------
    178     # HTML Template
    179 
    180     HTML_TMPL = r"""<!DOCTYPE html>
    181 <html lang="zh-cn">
    182   <head>
    183     <meta charset="utf-8">
    184     <meta http-equiv="X-UA-Compatible" content="IE=edge">
    185     <meta name="viewport" content="width=device-width, initial-scale=1">
    186     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    187     <title>%(title)s</title>
    188     <meta name="generator" content="%(generator)s"/>
    189     <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
    190     %(stylesheet)s
    191     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    192     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    193     <!--[if lt IE 9]>
    194       <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    195       <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
    196     <![endif]-->
    197   </head>
    198 <body>
    199 <script language="javascript" type="text/javascript"><!--
    200 output_list = Array();
    201 /* level - 0:Summary; 1:Failed; 2:All */
    202 function showCase(level) {
    203     trs = document.getElementsByTagName("tr");
    204     for (var i = 0; i < trs.length; i++) {
    205         tr = trs[i];
    206         id = tr.id;
    207         if (id.substr(0,2) == 'ft') {
    208             if (level < 1) {
    209                 tr.className = 'hiddenRow';
    210             }
    211             else {
    212                 tr.className = '';
    213             }
    214         }
    215         if (id.substr(0,2) == 'pt') {
    216             if (level > 1) {
    217                 tr.className = '';
    218             }
    219             else {
    220                 tr.className = 'hiddenRow';
    221             }
    222         }
    223     }
    224 }
    225 function showClassDetail(cid, count) {
    226     var id_list = Array(count);
    227     var toHide = 1;
    228     for (var i = 0; i < count; i++) {
    229         tid0 = 't' + cid.substr(1) + '.' + (i+1);
    230         tid = 'f' + tid0;
    231         tr = document.getElementById(tid);
    232         if (!tr) {
    233             tid = 'p' + tid0;
    234             tr = document.getElementById(tid);
    235         }
    236         id_list[i] = tid;
    237         if (tr.className) {
    238             toHide = 0;
    239         }
    240     }
    241     for (var i = 0; i < count; i++) {
    242         tid = id_list[i];
    243         if (toHide) {
    244             document.getElementById('div_'+tid).style.display = 'none'
    245             document.getElementById(tid).className = 'hiddenRow';
    246         }
    247         else {
    248             document.getElementById(tid).className = '';
    249         }
    250     }
    251 }
    252 function showTestDetail(div_id){
    253     var details_div = document.getElementById(div_id)
    254     var displayState = details_div.style.display
    255     // alert(displayState)
    256     if (displayState != 'block' ) {
    257         displayState = 'block'
    258         details_div.style.display = 'block'
    259     }
    260     else {
    261         details_div.style.display = 'none'
    262     }
    263 }
    264 function html_escape(s) {
    265     s = s.replace(/&/g,'&amp;');
    266     s = s.replace(/</g,'&lt;');
    267     s = s.replace(/>/g,'&gt;');
    268     return s;
    269 }
    270 /* obsoleted by detail in <div>
    271 function showOutput(id, name) {
    272     var w = window.open("", //url
    273                     name,
    274                     "resizable,scrollbars,status,width=800,height=450");
    275     d = w.document;
    276     d.write("<pre>");
    277     d.write(html_escape(output_list[id]));
    278     d.write("
    ");
    279     d.write("<a href='javascript:window.close()'>close</a>
    ");
    280     d.write("</pre>
    ");
    281     d.close();
    282 }
    283 */
    284 --></script>
    285 <div class="container">
    286     %(heading)s
    287     %(report)s
    288     %(ending)s
    289 </div>
    290 </body>
    291 </html>
    292 """
    293     # variables: (title, generator, stylesheet, heading, report, ending)
    294 
    295 
    296     # ------------------------------------------------------------------------
    297     # Stylesheet
    298     #
    299     # alternatively use a <link> for external style sheet, e.g.
    300     #   <link rel="stylesheet" href="$url" type="text/css">
    301 
    302     STYLESHEET_TMPL = """
    303 <style type="text/css" media="screen">
    304 /* -- css div popup ------------------------------------------------------------------------ */
    305 .popup_window {
    306     display: none;
    307     position: relative;
    308     left: 0px;
    309     top: 0px;
    310     /*border: solid #627173 1px; */
    311     padding: 10px;
    312     background-color: #99CCFF;
    313     font-family: "Lucida Console", "Courier New", Courier, monospace;
    314     text-align: left;
    315     font-size: 10pt;
    316      500px;
    317 }
    318 /* -- report ------------------------------------------------------------------------ */
    319 #show_detail_line .label {
    320     font-size: 85%;
    321     cursor: pointer;
    322 }
    323 #show_detail_line {
    324     margin: 2em auto 1em auto;
    325 }
    326 #total_row  { font-weight: bold; }
    327 .hiddenRow  { display: none; }
    328 .testcase   { margin-left: 2em; }
    329 </style>
    330 """
    331 
    332 
    333 
    334     # ------------------------------------------------------------------------
    335     # Heading
    336     #
    337 
    338     HEADING_TMPL = """<div class='heading'>
    339 <h1>%(title)s</h1>
    340 %(parameters)s
    341 <p class='description'>%(description)s</p>
    342 </div>
    343 """ # variables: (title, parameters, description)
    344 
    345     HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>
    346 """ # variables: (name, value)
    347 
    348 
    349 
    350     # ------------------------------------------------------------------------
    351     # Report
    352     #
    353 
    354     REPORT_TMPL = """
    355 <p id='show_detail_line'>
    356 <span class="label label-primary" onclick="showCase(0)">Summary</span>
    357 <span class="label label-danger" onclick="showCase(1)">Failed</span>
    358 <span class="label label-default" onclick="showCase(2)">All</span>
    359 </p>
    360 <table id='result_table' class="table">
    361     <thead>
    362         <tr id='header_row'>
    363             <th>Test Group/Test case</td>
    364             <th>Count</td>
    365             <th>Pass</td>
    366             <th>Fail</td>
    367             <th>Error</td>
    368             <th>View</td>
    369         </tr>
    370     </thead>
    371     <tbody>
    372         %(test_list)s
    373     </tbody>
    374     <tfoot>
    375         <tr id='total_row'>
    376             <td>Total</td>
    377             <td>%(count)s</td>
    378             <td class="text text-success">%(Pass)s</td>
    379             <td class="text text-danger">%(fail)s</td>
    380             <td class="text text-warning">%(error)s</td>
    381             <td>&nbsp;</td>
    382         </tr>
    383     </tfoot>
    384 </table>
    385 """ # variables: (test_list, count, Pass, fail, error)
    386 
    387     REPORT_CLASS_TMPL = r"""
    388 <tr class='%(style)s'>
    389     <td>%(desc)s</td>
    390     <td>%(count)s</td>
    391     <td>%(Pass)s</td>
    392     <td>%(fail)s</td>
    393     <td>%(error)s</td>
    394     <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
    395 </tr>
    396 """ # variables: (style, desc, count, Pass, fail, error, cid)
    397 
    398 
    399     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    400 <tr id='%(tid)s' class='%(Class)s'>
    401     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    402     <td colspan='5' align='center'>
    403     <!--css div popup start-->
    404     <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
    405         %(status)s</a>
    406     <div id='div_%(tid)s' class="popup_window">
    407         <div style='text-align: right;cursor:pointer'>
    408         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
    409            [x]</a>
    410         </div>
    411         <pre>
    412         %(script)s
    413         </pre>
    414     </div>
    415     <!--css div popup end-->
    416     </td>
    417 </tr>
    418 """ # variables: (tid, Class, style, desc, status)
    419 
    420 
    421     REPORT_TEST_NO_OUTPUT_TMPL = r"""
    422 <tr id='%(tid)s' class='%(Class)s'>
    423     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    424     <td colspan='5' align='center'>%(status)s</td>
    425 </tr>
    426 """ # variables: (tid, Class, style, desc, status)
    427 
    428 
    429     REPORT_TEST_OUTPUT_TMPL = r"""
    430 %(id)s: %(output)s
    431 """ # variables: (id, output)
    432 
    433 
    434 
    435     # ------------------------------------------------------------------------
    436     # ENDING
    437     #
    438 
    439     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    440 
    441 # -------------------- The end of the Template class -------------------
    442 
    443 
    444 TestResult = unittest.TestResult
    445 
    446 class _TestResult(TestResult):
    447     # note: _TestResult is a pure representation of results.
    448     # It lacks the output and reporting ability compares to unittest._TextTestResult.
    449 
    450     def __init__(self, verbosity=1):
    451         TestResult.__init__(self)
    452         self.outputBuffer = StringIO()
    453         self.stdout0 = None
    454         self.stderr0 = None
    455         self.success_count = 0
    456         self.failure_count = 0
    457         self.error_count = 0
    458         self.verbosity = verbosity
    459 
    460         # result is a list of result in 4 tuple
    461         # (
    462         #   result code (0: success; 1: fail; 2: error),
    463         #   TestCase object,
    464         #   Test output (byte string),
    465         #   stack trace,
    466         # )
    467         self.result = []
    468 
    469 
    470     def startTest(self, test):
    471         TestResult.startTest(self, test)
    472         # just one buffer for both stdout and stderr
    473         stdout_redirector.fp = self.outputBuffer
    474         stderr_redirector.fp = self.outputBuffer
    475         self.stdout0 = sys.stdout
    476         self.stderr0 = sys.stderr
    477         sys.stdout = stdout_redirector
    478         sys.stderr = stderr_redirector
    479 
    480 
    481     def complete_output(self):
    482         """
    483         Disconnect output redirection and return buffer.
    484         Safe to call multiple times.
    485         """
    486         if self.stdout0:
    487             sys.stdout = self.stdout0
    488             sys.stderr = self.stderr0
    489             self.stdout0 = None
    490             self.stderr0 = None
    491         return self.outputBuffer.getvalue()
    492 
    493 
    494     def stopTest(self, test):
    495         # Usually one of addSuccess, addError or addFailure would have been called.
    496         # But there are some path in unittest that would bypass this.
    497         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
    498         self.complete_output()
    499 
    500 
    501     def addSuccess(self, test):
    502         self.success_count += 1
    503         TestResult.addSuccess(self, test)
    504         output = self.complete_output()
    505         self.result.append((0, test, output, ''))
    506         if self.verbosity > 1:
    507             sys.stderr.write('ok ')
    508             sys.stderr.write(str(test))
    509             sys.stderr.write('
    ')
    510         else:
    511             sys.stderr.write('.')
    512 
    513     def addError(self, test, err):
    514         self.error_count += 1
    515         TestResult.addError(self, test, err)
    516         _, _exc_str = self.errors[-1]
    517         output = self.complete_output()
    518         self.result.append((2, test, output, _exc_str))
    519         if self.verbosity > 1:
    520             sys.stderr.write('E  ')
    521             sys.stderr.write(str(test))
    522             sys.stderr.write('
    ')
    523         else:
    524             sys.stderr.write('E')
    525 
    526     def addFailure(self, test, err):
    527         self.failure_count += 1
    528         TestResult.addFailure(self, test, err)
    529         _, _exc_str = self.failures[-1]
    530         output = self.complete_output()
    531         self.result.append((1, test, output, _exc_str))
    532         if self.verbosity > 1:
    533             sys.stderr.write('F  ')
    534             sys.stderr.write(str(test))
    535             sys.stderr.write('
    ')
    536         else:
    537             sys.stderr.write('F')
    538 
    539 
    540 class BSTestRunner(Template_mixin):
    541     """
    542     """
    543     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
    544         self.stream = stream
    545         self.verbosity = verbosity
    546         if title is None:
    547             self.title = self.DEFAULT_TITLE
    548         else:
    549             self.title = title
    550         if description is None:
    551             self.description = self.DEFAULT_DESCRIPTION
    552         else:
    553             self.description = description
    554 
    555         self.startTime = datetime.datetime.now()
    556 
    557 
    558     def run(self, test):
    559         "Run the given test case or test suite."
    560         result = _TestResult(self.verbosity)
    561         test(result)
    562         self.stopTime = datetime.datetime.now()
    563         self.generateReport(test, result)
    564         # print >>sys.stderr, '
    Time Elapsed: %s' % (self.stopTime-self.startTime)
    565         sys.stderr.write('
    Time Elapsed: %s' % (self.stopTime-self.startTime))
    566         return result
    567 
    568 
    569     def sortResult(self, result_list):
    570         # unittest does not seems to run in any particular order.
    571         # Here at least we want to group them together by class.
    572         rmap = {}
    573         classes = []
    574         for n,t,o,e in result_list:
    575             cls = t.__class__
    576             # if not rmap.has_key(cls):
    577             if not cls in rmap:
    578                 rmap[cls] = []
    579                 classes.append(cls)
    580             rmap[cls].append((n,t,o,e))
    581         r = [(cls, rmap[cls]) for cls in classes]
    582         return r
    583 
    584 
    585     def getReportAttributes(self, result):
    586         """
    587         Return report attributes as a list of (name, value).
    588         Override this to add custom attributes.
    589         """
    590         startTime = str(self.startTime)[:19]
    591         duration = str(self.stopTime - self.startTime)
    592         status = []
    593         if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>'    % result.success_count)
    594         if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
    595         if result.error_count:   status.append('<span class="text text-warning">Error <strong>%s</strong></span>'   % result.error_count  )
    596         if status:
    597             status = ' '.join(status)
    598         else:
    599             status = 'none'
    600         return [
    601             ('Start Time', startTime),
    602             ('Duration', duration),
    603             ('Status', status),
    604         ]
    605 
    606 
    607     def generateReport(self, test, result):
    608         report_attrs = self.getReportAttributes(result)
    609         generator = 'BSTestRunner %s' % __version__
    610         stylesheet = self._generate_stylesheet()
    611         heading = self._generate_heading(report_attrs)
    612         report = self._generate_report(result)
    613         ending = self._generate_ending()
    614         output = self.HTML_TMPL % dict(
    615             title = saxutils.escape(self.title),
    616             generator = generator,
    617             stylesheet = stylesheet,
    618             heading = heading,
    619             report = report,
    620             ending = ending,
    621         )
    622         try:
    623             self.stream.write(output.encode('utf8'))
    624         except:
    625             self.stream.write(output)
    626 
    627 
    628     def _generate_stylesheet(self):
    629         return self.STYLESHEET_TMPL
    630 
    631 
    632     def _generate_heading(self, report_attrs):
    633         a_lines = []
    634         for name, value in report_attrs:
    635             line = self.HEADING_ATTRIBUTE_TMPL % dict(
    636                     # name = saxutils.escape(name),
    637                     # value = saxutils.escape(value),
    638                     name = name,
    639                     value = value,
    640                 )
    641             a_lines.append(line)
    642         heading = self.HEADING_TMPL % dict(
    643             title = saxutils.escape(self.title),
    644             parameters = ''.join(a_lines),
    645             description = saxutils.escape(self.description),
    646         )
    647         return heading
    648 
    649 
    650     def _generate_report(self, result):
    651         rows = []
    652         sortedResult = self.sortResult(result.result)
    653         for cid, (cls, cls_results) in enumerate(sortedResult):
    654             # subtotal for a class
    655             np = nf = ne = 0
    656             for n,t,o,e in cls_results:
    657                 if n == 0: np += 1
    658                 elif n == 1: nf += 1
    659                 else: ne += 1
    660 
    661             # format class description
    662             if cls.__module__ == "__main__":
    663                 name = cls.__name__
    664             else:
    665                 name = "%s.%s" % (cls.__module__, cls.__name__)
    666             doc = cls.__doc__ and cls.__doc__.split("
    ")[0] or ""
    667             desc = doc and '%s: %s' % (name, doc) or name
    668 
    669             row = self.REPORT_CLASS_TMPL % dict(
    670                 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
    671                 desc = desc,
    672                 count = np+nf+ne,
    673                 Pass = np,
    674                 fail = nf,
    675                 error = ne,
    676                 cid = 'c%s' % (cid+1),
    677             )
    678             rows.append(row)
    679 
    680             for tid, (n,t,o,e) in enumerate(cls_results):
    681                 self._generate_report_test(rows, cid, tid, n, t, o, e)
    682 
    683         report = self.REPORT_TMPL % dict(
    684             test_list = ''.join(rows),
    685             count = str(result.success_count+result.failure_count+result.error_count),
    686             Pass = str(result.success_count),
    687             fail = str(result.failure_count),
    688             error = str(result.error_count),
    689         )
    690         return report
    691 
    692 
    693     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
    694         # e.g. 'pt1.1', 'ft1.1', etc
    695         has_output = bool(o or e)
    696         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
    697         name = t.id().split('.')[-1]
    698         doc = t.shortDescription() or ""
    699         desc = doc and ('%s: %s' % (name, doc)) or name
    700         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    701 
    702         # o and e should be byte string because they are collected from stdout and stderr?
    703         if isinstance(o,str):
    704             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    705             # uo = unicode(o.encode('string_escape'))
    706             try:
    707                 uo = o.decode('latin-1')
    708             except:
    709                 uo = o
    710         else:
    711             uo = o
    712         if isinstance(e,str):
    713             # TODO: some problem with 'string_escape': it escape 
     and mess up formating
    714             # ue = unicode(e.encode('string_escape'))
    715             try:
    716                 ue = e.decode('latin-1')
    717             except:
    718                 ue = e
    719         else:
    720             ue = e
    721 
    722         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
    723             id = tid,
    724             output = saxutils.escape(uo+ue),
    725         )
    726 
    727         row = tmpl % dict(
    728             tid = tid,
    729             # Class = (n == 0 and 'hiddenRow' or 'none'),
    730             Class = (n == 0 and 'hiddenRow' or 'text text-success'),
    731             # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
    732             style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
    733             desc = desc,
    734             script = script,
    735             status = self.STATUS[n],
    736         )
    737         rows.append(row)
    738         if not has_output:
    739             return
    740 
    741     def _generate_ending(self):
    742         return self.ENDING_TMPL
    743 
    744 
    745 ##############################################################################
    746 # Facilities for running tests from the command line
    747 ##############################################################################
    748 
    749 # Note: Reuse unittest.TestProgram to launch test. In the future we may
    750 # build our own launcher to support more specific command line
    751 # parameters like test title, CSS, etc.
    752 class TestProgram(unittest.TestProgram):
    753     """
    754     A variation of the unittest.TestProgram. Please refer to the base
    755     class for command line parameters.
    756     """
    757     def runTests(self):
    758         # Pick BSTestRunner as the default test runner.
    759         # base class's testRunner parameter is not useful because it means
    760         # we have to instantiate BSTestRunner before we know self.verbosity.
    761         if self.testRunner is None:
    762             self.testRunner = BSTestRunner(verbosity=self.verbosity)
    763         unittest.TestProgram.runTests(self)
    764 
    765 main = TestProgram
    766 
    767 ##############################################################################
    768 # Executing this module from the command line
    769 ##############################################################################
    770 
    771 if __name__ == "__main__":
    772     main(module=None)
    BSTestRunner.py for Python 2.x

    see also: HTMLTestRunner修改成Python3版本 | https://github.com/easonhan007/HTMLTestRunner | HTMLTestRunner.py for Python 3.x | BSTestRunner.py for Python 3.x | HTMLTestRunner.py for Python 2.x

    import webbrowser
    import unittest
    import HTMLTestRunner
    import BSTestRunner
     
     
    class TestStringMethods(unittest.TestCase):
     
        def test_upper(self):
            u"""判断 foo.upper() 是否等于 FOO"""
            self.assertEqual('foo'.upper(), 'FOO')
     
        def test_isupper(self):
            u""" 判断 Foo 是否为大写形式 """
            self.assertTrue('Foo'.isupper())
     
     
    if __name__ == '__main__':
        suite = unittest.makeSuite(TestStringMethods)
        f1 = open('result1.html''wb')
        f2 = open('result2.html''wb')
        HTMLTestRunner.HTMLTestRunner(
            stream=f1,
            title=u'HTMLTestRunner版本关于upper的测试报告',
            description=u'判断upper的测试用例执行情况').run(suite)
        suite = unittest.makeSuite(TestStringMethods)
        BSTestRunner.BSTestRunner(
            stream=f2,
            title=u'BSTestRunner版本关于upper的测试报告',
            description=u'判断upper的测试用例执行情况').run(suite)
        f1.close()
        f2.close()
        webbrowser.open('result1.html')
        webbrowser.open('result2.html')
  • 相关阅读:
    Java测试开发--Set、Map、List三种集合(四)
    Java测试开发--Maven用法(三)
    Java测试开发--Java基础知识(二)
    干净的卸载数据库
    腾讯云服务器部署springboot项目
    MultipartFile 实现图片上传
    URI和URL
    Redis debug模式报org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with...错误
    空指针异常
    Java 程序中怎么保证多线程的运行安全?
  • 原文地址:https://www.cnblogs.com/sundawei7/p/11947602.html
Copyright © 2011-2022 走看看