python3也支持的HTMLTestRunner
Posted jayson-0425
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python3也支持的HTMLTestRunner相关的知识,希望对你有一定的参考价值。
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(" "); 284 d.write("<a href=‘javascript:window.close()‘>close</a> "); 285 d.write("</pre> "); 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(‘ ‘) 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(‘ ‘) 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(‘ ‘) 603 else: 604 sys.stderr.write(‘F‘) 605 606 class HTMLTestRunner(Template_mixin): 607 """ 608 """ 609 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 610 self.stream = stream 611 self.verbosity = verbosity 612 if title is None: 613 self.title = self.DEFAULT_TITLE 614 else: 615 self.title = title 616 if description is None: 617 self.description = self.DEFAULT_DESCRIPTION 618 else: 619 self.description = description 620 621 self.startTime = datetime.datetime.now() 622 623 624 def run(self, test): 625 "Run the given test case or test suite." 626 result = _TestResult(self.verbosity) 627 test(result) 628 self.stopTime = datetime.datetime.now() 629 self.generateReport(test, result) 630 print (sys.stderr, ‘ Time Elapsed: %s‘ % (self.stopTime-self.startTime)) 631 return result 632 633 634 def sortResult(self, result_list): 635 # unittest does not seems to run in any particular order. 636 # Here at least we want to group them together by class. 637 rmap = {} 638 classes = [] 639 for n,t,o,e in result_list: 640 cls = t.__class__ 641 if not cls in rmap: 642 rmap[cls] = [] 643 classes.append(cls) 644 rmap[cls].append((n,t,o,e)) 645 r = [(cls, rmap[cls]) for cls in classes] 646 return r 647 648 649 def getReportAttributes(self, result): 650 """ 651 Return report attributes as a list of (name, value). 652 Override this to add custom attributes. 653 """ 654 startTime = str(self.startTime)[:19] 655 duration = str(self.stopTime - self.startTime) 656 status = [] 657 if result.success_count: status.append(‘Pass %s‘ % result.success_count) 658 if result.failure_count: status.append(‘Failure %s‘ % result.failure_count) 659 if result.error_count: status.append(‘Error %s‘ % result.error_count ) 660 if status: 661 status = ‘ ‘.join(status) 662 else: 663 status = ‘none‘ 664 return [ 665 (‘Start Time‘, startTime), 666 (‘Duration‘, duration), 667 (‘Status‘, status), 668 ] 669 670 671 def generateReport(self, test, result): 672 report_attrs = self.getReportAttributes(result) 673 generator = ‘HTMLTestRunner %s‘ % __version__ 674 stylesheet = self._generate_stylesheet() 675 heading = self._generate_heading(report_attrs) 676 report = self._generate_report(result) 677 ending = self._generate_ending() 678 output = self.HTML_TMPL % dict( 679 title = saxutils.escape(self.title), 680 generator = generator, 681 stylesheet = stylesheet, 682 heading = heading, 683 report = report, 684 ending = ending, 685 ) 686 self.stream.write(output.encode(‘utf8‘)) 687 688 689 def _generate_stylesheet(self): 690 return self.STYLESHEET_TMPL 691 692 693 def _generate_heading(self, report_attrs): 694 a_lines = [] 695 for name, value in report_attrs: 696 line = self.HEADING_ATTRIBUTE_TMPL % dict( 697 name = saxutils.escape(name), 698 value = saxutils.escape(value), 699 ) 700 a_lines.append(line) 701 heading = self.HEADING_TMPL % dict( 702 title = saxutils.escape(self.title), 703 parameters = ‘‘.join(a_lines), 704 description = saxutils.escape(self.description), 705 ) 706 return heading 707 708 709 def _generate_report(self, result): 710 rows = [] 711 sortedResult = self.sortResult(result.result) 712 for cid, (cls, cls_results) in enumerate(sortedResult): 713 # subtotal for a class 714 np = nf = ne = 0 715 for n,t,o,e in cls_results: 716 if n == 0: np += 1 717 elif n == 1: nf += 1 718 else: ne += 1 719 720 # format class description 721 if cls.__module__ == "__main__": 722 name = cls.__name__ 723 else: 724 name = "%s.%s" % (cls.__module__, cls.__name__) 725 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 726 desc = doc and ‘%s: %s‘ % (name, doc) or name 727 728 row = self.REPORT_CLASS_TMPL % dict( 729 style = ne > 0 and ‘errorClass‘ or nf > 0 and ‘failClass‘ or ‘passClass‘, 730 desc = desc, 731 count = np+nf+ne, 732 Pass = np, 733 fail = nf, 734 error = ne, 735 cid = ‘c%s‘ % (cid+1), 736 ) 737 rows.append(row) 738 739 for tid, (n,t,o,e) in enumerate(cls_results): 740 self._generate_report_test(rows, cid, tid, n, t, o, e) 741 742 report = self.REPORT_TMPL % dict( 743 test_list = ‘‘.join(rows), 744 count = str(result.success_count+result.failure_count+result.error_count), 745 Pass = str(result.success_count), 746 fail = str(result.failure_count), 747 error = str(result.error_count), 748 ) 749 return report 750 751 752 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 753 # e.g. ‘pt1.1‘, ‘ft1.1‘, etc 754 has_output = bool(o or e) 755 tid = (n == 0 and ‘p‘ or ‘f‘) + ‘t%s.%s‘ % (cid+1,tid+1) 756 name = t.id().split(‘.‘)[-1] 757 doc = t.shortDescription() or "" 758 desc = doc and (‘%s: %s‘ % (name, doc)) or name 759 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 760 761 # o and e should be byte string because they are collected from stdout and stderr? 762 if isinstance(o,str): 763 # TODO: some problem with ‘string_escape‘: it escape and mess up formating 764 # uo = unicode(o.encode(‘string_escape‘)) 765 uo = e 766 else: 767 uo = o 768 if isinstance(e,str): 769 # TODO: some problem with ‘string_escape‘: it escape and mess up formating 770 # ue = unicode(e.encode(‘string_escape‘)) 771 ue = e 772 else: 773 ue = e 774 775 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 776 id = tid, 777 output = saxutils.escape(uo+ue), 778 ) 779 780 row = tmpl % dict( 781 tid = tid, 782 Class = (n == 0 and ‘hiddenRow‘ or ‘none‘), 783 style = n == 2 and ‘errorCase‘ or (n == 1 and ‘failCase‘ or ‘none‘), 784 desc = desc, 785 script = script, 786 status = self.STATUS[n], 787 ) 788 rows.append(row) 789 if not has_output: 790 return 791 792 def _generate_ending(self): 793 return self.ENDING_TMPL 794 795 796 ############################################################################## 797 # Facilities for running tests from the command line 798 ############################################################################## 799 800 # Note: Reuse unittest.TestProgram to launch test. In the future we may 801 # build our own launcher to support more specific command line 802 # parameters like test title, CSS, etc. 803 class TestProgram(unittest.TestProgram): 804 """ 805 A variation of the unittest.TestProgram. Please refer to the base 806 class for command line parameters. 807 """ 808 def runTests(self): 809 # Pick HTMLTestRunner as the default test runner. 810 # base class‘s testRunner parameter is not useful because it means 811 # we have to instantiate HTMLTestRunner before we know self.verbosity. 812 if self.testRunner is None: 813 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 814 unittest.TestProgram.runTests(self) 815 816 main = TestProgram 817 818 ############################################################################## 819 # Executing this module from the command line 820 ############################################################################## 821 822 if __name__ == "__main__": 823 main(module=None)
以上是关于python3也支持的HTMLTestRunner的主要内容,如果未能解决你的问题,请参考以下文章
selenium3.4.3 + python3.6 + HTMLTestRunner0.8.0