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