首页 > 技术文章 > unittest生成测试报告

sundawei7 2019-11-28 11:08 原文

About

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

首先,Python2.x和Python3.x中两个扩展包不兼容(但下载和使用一致),这里以Python3.x为例。只是目前,无法通过pip安装。所以在使用之前,需要下载HTTLTestRunner.py,下载地址在文章最后的链接中。或者将下面的源码拷贝一份,文件名为HTTLTestRunner.py,保存在Python解释器的 \Lib\site-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("\n");
284     d.write("<a href='javascript:window.close()'>close</a>\n");
285     d.write("</pre>\n");
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     width: 500px;
353 }
354 
355 }
356 /* -- report ------------------------------------------------------------------------ */
357 #show_detail_line {
358     margin-top: 3ex;
359     margin-bottom: 1ex;
360 }
361 #result_table {
362     width: 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('\n')
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('\n')
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('\n')
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, '\nTime 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("\n")[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 \n 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 \n 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("\n");
308     d.write("<a href='javascript:window.close()'>close</a>\n");
309     d.write("</pre>\n");
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     width: 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('\n')
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('\n')
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('\n')
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, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
607         print(sys.stderr, '\nTime 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("\n")[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 \n 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 \n 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("\n");
284     d.write("<a href='javascript:window.close()'>close</a>\n");
285     d.write("</pre>\n");
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     width: 500px;
353 }
354 
355 }
356 /* -- report ------------------------------------------------------------------------ */
357 #show_detail_line {
358     margin-top: 3ex;
359     margin-bottom: 1ex;
360 }
361 #result_table {
362     width: 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('\n')
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('\n')
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('\n')
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, '\nTime 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("\n")[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 \n 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 \n 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("\n");
279     d.write("<a href='javascript:window.close()'>close</a>\n");
280     d.write("</pre>\n");
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     width: 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('\n')
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('\n')
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('\n')
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, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
565         sys.stderr.write('\nTime 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("\n")[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 \n 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 \n 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')

推荐阅读