生成 HTMLTestRunner 模块

  • unittest 里面是不能生成 html 格式报告的,需要导入一个第三方的模块:HTMLTestRunner
  • 方法1.这个模块下载不能通过 pip 安装了,只能下载后手动导入,下载地址:http://tungwaiyip.info/software/HTMLTestRunner.html
  • 方法2.在 python 安装文件的 Lib 目录下新增文件 HTMLTestRunner.py
    • 两种模板如下,建议使用第一种(第一种模板更加美观)

文件内容如下:

(1)第一种模板

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

(2)第二种模板

  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,'&');
270     s = s.replace(/</g,'<');
271     s = s.replace(/>/g,'>');
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> </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'> </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.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, "\nTimeElapsed: %s" % (self.stopTime-self.startTime))
632         sys.stderr.write('\nTime Elapsed: %s\n' % (self.stopTime - self.startTime))
633         return result
634 
635 
636     def sortResult(self, result_list):
637         # unittest does not seems to run in any particular order.
638         # Here at least we want to group them together by class.
639         rmap = {}
640         classes = []
641         for n,t,o,e in result_list:
642             cls = t.__class__
643             if not cls in rmap:
644                 rmap[cls] = []
645                 classes.append(cls)
646             rmap[cls].append((n,t,o,e))
647         r = [(cls, rmap[cls]) for cls in classes]
648         return r
649 
650 
651     def getReportAttributes(self, result):
652         """
653         Return report attributes as a list of (name, value).
654         Override this to add custom attributes.
655         """
656         startTime = str(self.startTime)[:19]
657         duration = str(self.stopTime - self.startTime)
658         status = []
659         if result.success_count: status.append('Pass %s'    % result.success_count)
660         if result.failure_count: status.append('Failure %s' % result.failure_count)
661         if result.error_count:   status.append('Error %s'   % result.error_count  )
662         if status:
663             status = ' '.join(status)
664         else:
665             status = 'none'
666         return [
667             ('Start Time', startTime),
668             ('Duration', duration),
669             ('Status', status),
670         ]
671 
672 
673     def generateReport(self, test, result):
674         report_attrs = self.getReportAttributes(result)
675         generator = 'HTMLTestRunner %s' % __version__
676         stylesheet = self._generate_stylesheet()
677         heading = self._generate_heading(report_attrs)
678         report = self._generate_report(result)
679         ending = self._generate_ending()
680         output = self.HTML_TMPL % dict(
681             title = saxutils.escape(self.title),
682             generator = generator,
683             stylesheet = stylesheet,
684             heading = heading,
685             report = report,
686             ending = ending,
687         )
688         self.stream.write(output.encode('utf8'))
689 
690 
691     def _generate_stylesheet(self):
692         return self.STYLESHEET_TMPL
693 
694 
695     def _generate_heading(self, report_attrs):
696         a_lines = []
697         for name, value in report_attrs:
698             line = self.HEADING_ATTRIBUTE_TMPL % dict(
699                     name = saxutils.escape(name),
700                     value = saxutils.escape(value),
701                 )
702             a_lines.append(line)
703         heading = self.HEADING_TMPL % dict(
704             title = saxutils.escape(self.title),
705             parameters = ''.join(a_lines),
706             description = saxutils.escape(self.description),
707         )
708         return heading
709 
710 
711     def _generate_report(self, result):
712         rows = []
713         sortedResult = self.sortResult(result.result)
714         for cid, (cls, cls_results) in enumerate(sortedResult):
715             # subtotal for a class
716             np = nf = ne = 0
717             for n,t,o,e in cls_results:
718                 if n == 0: np += 1
719                 elif n == 1: nf += 1
720                 else: ne += 1
721 
722             # format class description
723             if cls.__module__ == "__main__":
724                 name = cls.__name__
725             else:
726                 name = "%s.%s" % (cls.__module__, cls.__name__)
727             doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
728             desc = doc and '%s: %s' % (name, doc) or name
729 
730             row = self.REPORT_CLASS_TMPL % dict(
731                 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
732                 desc = desc,
733                 count = np+nf+ne,
734                 Pass = np,
735                 fail = nf,
736                 error = ne,
737                 cid = 'c%s' % (cid+1),
738             )
739             rows.append(row)
740 
741             for tid, (n,t,o,e) in enumerate(cls_results):
742                 self._generate_report_test(rows, cid, tid, n, t, o, e)
743 
744         report = self.REPORT_TMPL % dict(
745             test_list = ''.join(rows),
746             count = str(result.success_count+result.failure_count+result.error_count),
747             Pass = str(result.success_count),
748             fail = str(result.failure_count),
749             error = str(result.error_count),
750         )
751         return report
752 
753 
754     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
755         # e.g. 'pt1.1', 'ft1.1', etc
756         has_output = bool(o or e)
757         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
758         name = t.id().split('.')[-1]
759         doc = t.shortDescription() or ""
760         desc = doc and ('%s: %s' % (name, doc)) or name
761         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
762 
763         # o and e should be byte string because they are collected from stdout and stderr?
764         if isinstance(o,str):
765             # TODO: some problem with 'string_escape': it escape \n and mess up formating
766             # uo = unicode(o.encode('string_escape'))
767             uo = o
768         else:
769             uo = o
770         if isinstance(e,str):
771             # TODO: some problem with 'string_escape': it escape \n and mess up formating
772             # ue = unicode(e.encode('string_escape'))
773             ue = e
774         else:
775             ue = e
776 
777         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
778             id = tid,
779             output = saxutils.escape(uo+ue),
780         )
781 
782         row = tmpl % dict(
783             tid = tid,
784             Class = (n == 0 and 'hiddenRow' or 'none'),
785             style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
786             desc = desc,
787             script = script,
788             status = self.STATUS[n],
789         )
790         rows.append(row)
791         if not has_output:
792             return
793 
794     def _generate_ending(self):
795         return self.ENDING_TMPL
796 
797 
798 ##############################################################################
799 # Facilities for running tests from the command line
800 ##############################################################################
801 
802 # Note: Reuse unittest.TestProgram to launch test. In the future we may
803 # build our own launcher to support more specific command line
804 # parameters like test title, CSS, etc.
805 class TestProgram(unittest.TestProgram):
806     """
807     A variation of the unittest.TestProgram. Please refer to the base
808     class for command line parameters.
809     """
810     def runTests(self):
811         # Pick HTMLTestRunner as the default test runner.
812         # base class's testRunner parameter is not useful because it means
813         # we have to instantiate HTMLTestRunner before we know self.verbosity.
814         if self.testRunner is None:
815             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
816         unittest.TestProgram.runTests(self)
817 
818 main = TestProgram
819 
820 ##############################################################################
821 # Executing this module from the command line
822 ##############################################################################
823 
824 if __name__ == "__main__":
825     main(module=None)

 

# -*- coding: utf-8 -*-"""A TestRunner for use with the Python unit testing framework. Itgenerates a HTML report to show the result at a glance.The simplest way to use this is to invoke its main method. E.g.    import unittest    import HTMLTestRunner    ... define your tests ...    if __name__ == '__main__':        HTMLTestRunner.main()For more customization options, instantiates a HTMLTestRunner object.HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.    # output to a file    fp = file('my_report.html', 'wb')    runner = HTMLTestRunner.HTMLTestRunner(                stream=fp,                title='My unit test',                description='This demonstrates the report output by HTMLTestRunner.'                )    # Use an external stylesheet.    # See the Template_mixin class for more customizable options    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'    # run the test    runner.run(my_test_suite)------------------------------------------------------------------------Copyright (c) 2004-2007, Wai Yip TungAll rights reserved.Redistribution and use in source and binary forms, with or withoutmodification, are permitted provided that the following conditions aremet:* Redistributions of source code must retain the above copyright notice,  this list of conditions and the following disclaimer.* Redistributions in binary form must reproduce the above copyright  notice, this list of conditions and the following disclaimer in the  documentation and/or other materials provided with the distribution.* Neither the name Wai Yip Tung nor the names of its contributors may be  used to endorse or promote products derived from this software without  specific prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "ASIS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR APARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNEROR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, ORPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OFLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDINGNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THISSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""
# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
__author__ = "Wai Yip Tung"__version__ = "0.9.1"
"""Change HistoryVersion 0.9.1* 用Echarts添加执行情况统计图 (灰蓝)Version 0.9.0* 改成Python 3.x (灰蓝)Version 0.8.3* 使用 Bootstrap稍加美化 (灰蓝)* 改为中文 (灰蓝)Version 0.8.2* Show output inline instead of popup window (Viorel Lupu).Version in 0.8.1* Validated XHTML (Wolfgang Borgert).* Added description of test classes and test cases.Version in 0.8.0* Define Template_mixin class for customization.* Workaround a IE 6 bug that it does not treat <script> block as CDATA.Version in 0.7.1* Back port to Python 2.3 (Frank Horowitz).* Fix missing scroll bars in detail log (Podi)."""
# TODO: color stderr# TODO: simplify javascript using ,ore than 1 class in the class attribute?
import datetimeimport sysimport ioimport timeimport unittestfrom xml.sax import saxutilsimport getpass

# ------------------------------------------------------------------------# The redirectors below are used to capture output during testing. Output# sent to sys.stdout and sys.stderr are automatically captured. However# in some cases sys.stdout is already cached before HTMLTestRunner is# invoked (e.g. calling logging.basicConfig). In order to capture those# output, use the redirectors for the cached stream.## e.g.#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)#   >>>
class OutputRedirector(object):    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):        self.fp = fp
    def write(self, s):        self.fp.write(s)
    def writelines(self, lines):        self.fp.writelines(lines)
    def flush(self):        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)stderr_redirector = OutputRedirector(sys.stderr)

# ----------------------------------------------------------------------# Template

class Template_mixin(object):    """    Define a HTML template for report customerization and generation.    Overall structure of an HTML report    HTML    +------------------------+    |<html>                  |    |  <head>                |    |                        |    |   STYLESHEET           |    |   +----------------+   |    |   |                |   |    |   +----------------+   |    |                        |    |  </head>               |    |                        |    |  <body>                |    |                        |    |   HEADING              |    |   +----------------+   |    |   |                |   |    |   +----------------+   |    |                        |    |   REPORT               |    |   +----------------+   |    |   |                |   |    |   +----------------+   |    |                        |    |   ENDING               |    |   +----------------+   |    |   |                |   |    |   +----------------+   |    |                        |    |  </body>               |    |</html>                 |    +------------------------+    """
    STATUS = {        0: u'通过',        1: u'失败',        2: u'错误',    }
    DEFAULT_TITLE = 'Unit Test Report'    DEFAULT_DESCRIPTION = ''
    # ------------------------------------------------------------------------    # HTML Template
    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head>    <title>%(title)s</title>    <meta name="generator" content="%(generator)s"/>    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">    <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script>    <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
    %(stylesheet)s
</head><body>    <script language="javascript" type="text/javascript"><!--    output_list = Array();    /* level - 0:Summary; 1:Failed; 2:All */    function showCase(level) {        trs = document.getElementsByTagName("tr");        for (var i = 0; i < trs.length; i++) {            tr = trs[i];            id = tr.id;            if (id.substr(0,2) == 'ft') {                if (level < 1) {                    tr.className = 'hiddenRow';                }                else {                    tr.className = '';                }            }            if (id.substr(0,2) == 'pt') {                if (level > 1) {                    tr.className = '';                }                else {                    tr.className = 'hiddenRow';                }            }        }    }    function showClassDetail(cid, count) {        var id_list = Array(count);        var toHide = 1;        for (var i = 0; i < count; i++) {            tid0 = 't' + cid.substr(1) + '.' + (i+1);            tid = 'f' + tid0;            tr = document.getElementById(tid);            if (!tr) {                tid = 'p' + tid0;                tr = document.getElementById(tid);            }            id_list[i] = tid;            if (tr.className) {                toHide = 0;            }        }        for (var i = 0; i < count; i++) {            tid = id_list[i];            if (toHide) {                document.getElementById('div_'+tid).style.display = 'none'                document.getElementById(tid).className = 'hiddenRow';            }            else {                document.getElementById(tid).className = '';            }        }    }    function showTestDetail(div_id){        var details_div = document.getElementById(div_id)        var displayState = details_div.style.display        // alert(displayState)        if (displayState != 'block' ) {            displayState = 'block'            details_div.style.display = 'block'        }        else {            details_div.style.display = 'none'        }    }    function html_escape(s) {        s = s.replace(/&/g,'&');        s = s.replace(/</g,'<');        s = s.replace(/>/g,'>');        return s;    }    /* obsoleted by detail in <div>    function showOutput(id, name) {        var w = window.open("", //url                        name,                        "resizable,scrollbars,status,width=800,height=450");        d = w.document;        d.write("<pre>");        d.write(html_escape(output_list[id]));        d.write("\n");        d.write("<a href='javascript:window.close()'>close</a>\n");        d.write("</pre>\n");        d.close();    }    */    --></script>    <div id="div_base">        %(heading)s        %(report)s        %(ending)s        %(chart_script)s    </div></body></html>"""  # variables: (title, generator, stylesheet, heading, report, ending, chart_script)
    ECHARTS_SCRIPT = """    <script type="text/javascript">        // 基于准备好的dom,初始化echarts实例        var myChart = echarts.init(document.getElementById('chart'));        // 指定图表的配置项和数据        var option = {            title : {                text: '测试执行情况',                x:'center'            },            tooltip : {                trigger: 'item',                formatter: "{a} <br/>{b} : {c} ({d}%%)"            },            color: ['#95b75d', 'grey', '#b64645'],            legend: {                orient: 'vertical',                left: 'left',                data: ['通过','失败','错误']            },            series : [                {                    name: '测试执行情况',                    type: 'pie',                    radius : '60%%',                    center: ['50%%', '60%%'],                    data:[                        {value:%(Pass)s, name:'通过'},                        {value:%(fail)s, name:'失败'},                        {value:%(error)s, name:'错误'}                    ],                    itemStyle: {                        emphasis: {                            shadowBlur: 10,                            shadowOffsetX: 0,                            shadowColor: 'rgba(0, 0, 0, 0.5)'                        }                    }                }            ]        };        // 使用刚指定的配置项和数据显示图表。        myChart.setOption(option);    </script>    """  # variables: (Pass, fail, error)
    # ------------------------------------------------------------------------    # Stylesheet    #    # alternatively use a <link> for external style sheet, e.g.    #   <link rel="stylesheet" href="$url" type="text/css">
    STYLESHEET_TMPL = """<style type="text/css" media="screen">    body        { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; }    table       { font-size: 100%; }    pre         { white-space: pre-wrap;word-wrap: break-word; }    /* -- heading ---------------------------------------------------------------------- */    h1 {        font-size: 16pt;        color: gray;    }    .heading {        margin-top: 0ex;        margin-bottom: 1ex;    }    .heading .attribute {        margin-top: 1ex;        margin-bottom: 0;    }    .heading .description {        margin-top: 2ex;        margin-bottom: 3ex;    }    /* -- css div popup ------------------------------------------------------------------------ */    a.popup_link {    }    a.popup_link:hover {        color: red;    }    .popup_window {        display: none;        position: relative;        left: 0px;        top: 0px;        /*border: solid #627173 1px; */        padding: 10px;        /*background-color: #E6E6D6; */        font-family: "Lucida Console", "Courier New", Courier, monospace;        text-align: left;        font-size: 8pt;        /* width: 500px;*/    }    }    /* -- report ------------------------------------------------------------------------ */    #show_detail_line {        margin-top: 3ex;        margin-bottom: 1ex;    }    #result_table {        width: 99%;    }    #header_row {        font-weight: bold;        color: #303641;        background-color: #ebebeb;    }    #total_row  { font-weight: bold; }    .passClass  { background-color: #bdedbc; }    .failClass  { background-color: #ffefa4; }    .errorClass { background-color: #ffc9c9; }    .passCase   { color: #6c6; }    .failCase   { color: #FF6600; font-weight: bold; }    .errorCase  { color: #c00; font-weight: bold; }    .hiddenRow  { display: none; }    .testcase   { margin-left: 2em; }    /* -- ending ---------------------------------------------------------------------- */    #ending {    }    #div_base {                position:absolute;                top:0%;                left:5%;                right:5%;                width: auto;                height: auto;                margin: -15px 0 0 0;    }</style>"""
    # ------------------------------------------------------------------------    # Heading    #
    HEADING_TMPL = """    <div class='page-header'>        <h1>%(title)s</h1>    %(parameters)s    </div>    <div style="float: left;width:50%%;"><p class='description'>%(description)s</p></div>    <div id="chart" style="width:50%%;height:400px;float:left;"></div>"""  # variables: (title, parameters, description)
    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>"""  # variables: (name, value)
    # ------------------------------------------------------------------------    # Report    #
    REPORT_TMPL = u"""    <div class="btn-group btn-group-sm">        <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button>        <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>        <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>    </div>    <p></p>    <table id='result_table' class="table table-bordered">        <colgroup>            <col align='left' />            <col align='right' />            <col align='right' />            <col align='right' />            <col align='right' />            <col align='right' />        </colgroup>        <tr id='header_row'>            <td>测试套件/测试用例</td>            <td>总数</td>            <td>通过</td>            <td>失败</td>            <td>错误</td>            <td>查看</td>        </tr>        %(test_list)s        <tr id='total_row'>            <td>总计</td>            <td>%(count)s</td>            <td>%(Pass)s</td>            <td>%(fail)s</td>            <td>%(error)s</td>            <td> </td>        </tr>    </table>"""  # variables: (test_list, count, Pass, fail, error)
    REPORT_CLASS_TMPL = u"""    <tr class='%(style)s'>        <td>%(desc)s</td>        <td>%(count)s</td>        <td>%(Pass)s</td>        <td>%(fail)s</td>        <td>%(error)s</td>        <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>    </tr>"""  # variables: (style, desc, count, Pass, fail, error, cid)
    REPORT_TEST_WITH_OUTPUT_TMPL = r"""<tr id='%(tid)s' class='%(Class)s'>    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>    <td colspan='5' align='center'>    <!--css div popup start-->    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >        %(status)s</a>    <div id='div_%(tid)s' class="popup_window">        <pre>%(script)s</pre>    </div>    <!--css div popup end-->    </td></tr>"""  # variables: (tid, Class, style, desc, status)
    REPORT_TEST_NO_OUTPUT_TMPL = r"""<tr id='%(tid)s' class='%(Class)s'>    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>    <td colspan='5' align='center'>%(status)s</td></tr>"""  # variables: (tid, Class, style, desc, status)
    REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s"""  # variables: (id, output)
    # ------------------------------------------------------------------------    # ENDING    #
    ENDING_TMPL = """<div id='ending'> </div>"""

# -------------------- The end of the Template class -------------------

TestResult = unittest.TestResult

class _TestResult(TestResult):    # note: _TestResult is a pure representation of results.    # It lacks the output and reporting ability compares to unittest._TextTestResult.
    def __init__(self, verbosity=1):        TestResult.__init__(self)        self.stdout0 = None        self.stderr0 = None        self.success_count = 0        self.failure_count = 0        self.error_count = 0        self.verbosity = verbosity
        # result is a list of result in 4 tuple        # (        #   result code (0: success; 1: fail; 2: error),        #   TestCase object,        #   Test output (byte string),        #   stack trace,        # )        self.result = []        self.subtestlist = []
    def startTest(self, test):        TestResult.startTest(self, test)        # just one buffer for both stdout and stderr        self.outputBuffer = io.StringIO()        stdout_redirector.fp = self.outputBuffer        stderr_redirector.fp = self.outputBuffer        self.stdout0 = sys.stdout        self.stderr0 = sys.stderr        sys.stdout = stdout_redirector        sys.stderr = stderr_redirector
    def complete_output(self):        """        Disconnect output redirection and return buffer.        Safe to call multiple times.        """        if self.stdout0:            sys.stdout = self.stdout0            sys.stderr = self.stderr0            self.stdout0 = None            self.stderr0 = None        return self.outputBuffer.getvalue()
    def stopTest(self, test):        # Usually one of addSuccess, addError or addFailure would have been called.        # But there are some path in unittest that would bypass this.        # We must disconnect stdout in stopTest(), which is guaranteed to be called.        self.complete_output()
    def addSuccess(self, test):        if test not in self.subtestlist:            self.success_count += 1            TestResult.addSuccess(self, test)            output = self.complete_output()            self.result.append((0, test, output, ''))            if self.verbosity > 1:                sys.stderr.write('ok ')                sys.stderr.write(str(test))                sys.stderr.write('\n')            else:                sys.stderr.write('.')
    def addError(self, test, err):        self.error_count += 1        TestResult.addError(self, test, err)        _, _exc_str = self.errors[-1]        output = self.complete_output()        self.result.append((2, test, output, _exc_str))        if self.verbosity > 1:            sys.stderr.write('E  ')            sys.stderr.write(str(test))            sys.stderr.write('\n')        else:            sys.stderr.write('E')
    def addFailure(self, test, err):        self.failure_count += 1        TestResult.addFailure(self, test, err)        _, _exc_str = self.failures[-1]        output = self.complete_output()        self.result.append((1, test, output, _exc_str))        if self.verbosity > 1:            sys.stderr.write('F  ')            sys.stderr.write(str(test))            sys.stderr.write('\n')        else:            sys.stderr.write('F')
    def addSubTest(self, test, subtest, err):        if err is not None:            if getattr(self, 'failfast', False):                self.stop()            if issubclass(err[0], test.failureException):                self.failure_count += 1                errors = self.failures                errors.append((subtest, self._exc_info_to_string(err, subtest)))                output = self.complete_output()                self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest),                                    self._exc_info_to_string(err, subtest)))                if self.verbosity > 1:                    sys.stderr.write('F  ')                    sys.stderr.write(str(subtest))                    sys.stderr.write('\n')                else:                    sys.stderr.write('F')            else:                self.error_count += 1                errors = self.errors                errors.append((subtest, self._exc_info_to_string(err, subtest)))                output = self.complete_output()                self.result.append(                    (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest)))                if self.verbosity > 1:                    sys.stderr.write('E  ')                    sys.stderr.write(str(subtest))                    sys.stderr.write('\n')                else:                    sys.stderr.write('E')            self._mirrorOutput = True        else:            self.subtestlist.append(subtest)            self.subtestlist.append(test)            self.success_count += 1            output = self.complete_output()            self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), ''))            if self.verbosity > 1:                sys.stderr.write('ok ')                sys.stderr.write(str(subtest))                sys.stderr.write('\n')            else:                sys.stderr.write('.')
class HTMLTestRunner(Template_mixin):
    def __init__(self, stream=sys.stdout, verbosity=1, title="TestReport", tester=getpass.getuser(), description="测试详情如下:"):        self.stream = stream        self.verbosity = verbosity        self.tester = tester        """        verbosity:            =1的时候 默认值为1,不限制完整结果,即单个用例成功输出’.’,失败输出’F’,错误输出’E’            =0的时候。不输出信息            =2的时候,需要打印详细的返回信息        stream:测试报告写入文件的存储区域        title:测试报告的主题        tester:默认获取本机用户名        description:测试报告的描述        """        if title is None:            self.title = self.DEFAULT_TITLE        else:            self.title = title        if description is None:            self.description = self.DEFAULT_DESCRIPTION        else:            self.description = description
        self.startTime = datetime.datetime.now()
    def run(self, test):        "Run the given test case or test suite."        result = _TestResult(self.verbosity)        test(result)        self.stopTime = datetime.datetime.now()        self.generateReport(test, result)        print('\nTime Elapsed: %s' % (self.stopTime - self.startTime), file=sys.stderr)        return result
    def sortResult(self, result_list):        # unittest does not seems to run in any particular order.        # Here at least we want to group them together by class.        rmap = {}        classes = []        for n, t, o, e in result_list:            cls = t.__class__            if cls not in rmap:                rmap[cls] = []                classes.append(cls)            rmap[cls].append((n, t, o, e))        r = [(cls, rmap[cls]) for cls in classes]        return r
    def getReportAttributes(self, result):        """        Return report attributes as a list of (name, value).        Override this to add custom attributes.        """        startTime = str(self.startTime)[:19]        duration = str(self.stopTime - self.startTime)        status = []        if result.success_count: status.append(u'通过 %s' % result.success_count)        if result.failure_count: status.append(u'失败 %s' % result.failure_count)        if result.error_count:   status.append(u'错误 %s' % result.error_count)        if status:            status = ' '.join(status)        else:            status = 'none'        return [            (u'测试人员', self.tester),            (u'开始时间', startTime),            (u'运行时长', duration),            (u'状态', status)        ]
    def generateReport(self, test, result):        report_attrs = self.getReportAttributes(result)        generator = 'HTMLTestRunner %s' % __version__        stylesheet = self._generate_stylesheet()        heading = self._generate_heading(report_attrs)        report = self._generate_report(result)        ending = self._generate_ending()        chart = self._generate_chart(result)        output = self.HTML_TMPL % dict(            title=saxutils.escape(self.title),            generator=generator,            stylesheet=stylesheet,            heading=heading,            report=report,            ending=ending,            chart_script=chart        )        self.stream.write(output.encode('utf8'))
    def _generate_stylesheet(self):        return self.STYLESHEET_TMPL
    def _generate_heading(self, report_attrs):        a_lines = []        for name, value in report_attrs:            line = self.HEADING_ATTRIBUTE_TMPL % dict(                name=saxutils.escape(name),                value=saxutils.escape(value),            )            a_lines.append(line)        heading = self.HEADING_TMPL % dict(            title=saxutils.escape(self.title),            parameters=''.join(a_lines),            description=saxutils.escape(self.description),        )        return heading
    def _generate_report(self, result):        rows = []        sortedResult = self.sortResult(result.result)        for cid, (cls, cls_results) in enumerate(sortedResult):            # subtotal for a class            np = nf = ne = 0            for n, t, o, e in cls_results:                if n == 0:                    np += 1                elif n == 1:                    nf += 1                else:                    ne += 1
            # format class description            if cls.__module__ == "__main__":                name = cls.__name__            else:                name = "%s.%s" % (cls.__module__, cls.__name__)            doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""            desc = doc and '%s: %s' % (name, doc) or name
            row = self.REPORT_CLASS_TMPL % dict(                style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',                desc=desc,                count=np + nf + ne,                Pass=np,                fail=nf,                error=ne,                cid='c%s' % (cid + 1),            )            rows.append(row)
            for tid, (n, t, o, e) in enumerate(cls_results):                self._generate_report_test(rows, cid, tid, n, t, o, e)
        report = self.REPORT_TMPL % dict(            test_list=''.join(rows),            count=str(result.success_count + result.failure_count + result.error_count),            Pass=str(result.success_count),            fail=str(result.failure_count),            error=str(result.error_count),        )        return report
    def _generate_chart(self, result):        chart = self.ECHARTS_SCRIPT % dict(            Pass=str(result.success_count),            fail=str(result.failure_count),            error=str(result.error_count),        )        return chart
    def _generate_report_test(self, rows, cid, tid, n, t, o, e):        # e.g. 'pt1.1', 'ft1.1', etc        has_output = bool(o or e)        tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1)        name = t.id().split('.')[-1]        doc = t.shortDescription() or ""        desc = doc and ('%s: %s' % (name, doc)) or name        tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
        script = self.REPORT_TEST_OUTPUT_TMPL % dict(            id=tid,            output=saxutils.escape(o + e),        )
        row = tmpl % dict(            tid=tid,            Class=(n == 0 and 'hiddenRow' or 'none'),            style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),            desc=desc,            script=script,            status=self.STATUS[n],        )        rows.append(row)        if not has_output:            return
    def _generate_ending(self):        return self.ENDING_TMPL

############################################################################### Facilities for running tests from the command line##############################################################################
# Note: Reuse unittest.TestProgram to launch test. In the future we may# build our own launcher to support more specific command line# parameters like test title, CSS, etc.class TestProgram(unittest.TestProgram):    """    A variation of the unittest.TestProgram. Please refer to the base    class for command line parameters.    """
    def runTests(self):        # Pick HTMLTestRunner as the default test runner.        # base class's testRunner parameter is not useful because it means        # we have to instantiate HTMLTestRunner before we know self.verbosity.        if self.testRunner is None:            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)        unittest.TestProgram.runTests(self)

main = TestProgram
############################################################################### Executing this module from the command line##############################################################################
if __name__ == "__main__":    main(module=None)

posted on 2022-03-30 15:04  一只小小的测试呀  阅读(229)  评论(0)    收藏  举报

导航

……