1. 程式人生 > 其它 >生成 HTMLTestRunner 模組

生成 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)