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