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