├── HTMLTestRunner.py ├── README ├── sample_test_report.html └── test_HTMLTestRunner.py /HTMLTestRunner.py: -------------------------------------------------------------------------------- 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 = '' 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.3" 69 | 70 | 71 | """ 72 | Change History 73 | 74 | Version 0.8.3 75 | * Prevent crash on class or module-level exceptions (Darren Wurf). 76 | 77 | Version 0.8.2 78 | * Show output inline instead of popup window (Viorel Lupu). 79 | 80 | Version in 0.8.1 81 | * Validated XHTML (Wolfgang Borgert). 82 | * Added description of test classes and test cases. 83 | 84 | Version in 0.8.0 85 | * Define Template_mixin class for customization. 86 | * Workaround a IE 6 bug that it does not treat 301 | 302 | %(heading)s 303 | %(report)s 304 | %(ending)s 305 | 306 | 307 | 308 | """ 309 | # variables: (title, generator, stylesheet, heading, report, ending) 310 | 311 | 312 | # ------------------------------------------------------------------------ 313 | # Stylesheet 314 | # 315 | # alternatively use a for external style sheet, e.g. 316 | # 317 | 318 | STYLESHEET_TMPL = """ 319 | 402 | """ 403 | 404 | 405 | 406 | # ------------------------------------------------------------------------ 407 | # Heading 408 | # 409 | 410 | HEADING_TMPL = """
411 |

%(title)s

412 | %(parameters)s 413 |

%(description)s

414 |
415 | 416 | """ # variables: (title, parameters, description) 417 | 418 | HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

419 | """ # variables: (name, value) 420 | 421 | 422 | 423 | # ------------------------------------------------------------------------ 424 | # Report 425 | # 426 | 427 | REPORT_TMPL = """ 428 |

Show 429 | Summary 430 | Failed 431 | All 432 |

433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | %(test_list)s 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 |
Test Group/Test caseCountPassFailErrorView
Total%(count)s%(Pass)s%(fail)s%(error)s 
460 | """ # variables: (test_list, count, Pass, fail, error) 461 | 462 | REPORT_CLASS_TMPL = r""" 463 | 464 | %(desc)s 465 | %(count)s 466 | %(Pass)s 467 | %(fail)s 468 | %(error)s 469 | Detail 470 | 471 | """ # variables: (style, desc, count, Pass, fail, error, cid) 472 | 473 | 474 | REPORT_TEST_WITH_OUTPUT_TMPL = r""" 475 | 476 |
%(desc)s
477 | 478 | 479 | 480 | 481 | %(status)s 482 | 483 | 492 | 493 | 494 | 495 | 496 | """ # variables: (tid, Class, style, desc, status) 497 | 498 | 499 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 500 | 501 |
%(desc)s
502 | %(status)s 503 | 504 | """ # variables: (tid, Class, style, desc, status) 505 | 506 | 507 | REPORT_TEST_OUTPUT_TMPL = r""" 508 | %(id)s: %(output)s 509 | """ # variables: (id, output) 510 | 511 | 512 | 513 | # ------------------------------------------------------------------------ 514 | # ENDING 515 | # 516 | 517 | ENDING_TMPL = """
 
""" 518 | 519 | # -------------------- The end of the Template class ------------------- 520 | 521 | 522 | TestResult = unittest.TestResult 523 | 524 | class _TestResult(TestResult): 525 | # note: _TestResult is a pure representation of results. 526 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 527 | 528 | def __init__(self, verbosity=1): 529 | TestResult.__init__(self) 530 | self.outputBuffer = StringIO.StringIO() 531 | self.stdout0 = None 532 | self.stderr0 = None 533 | self.success_count = 0 534 | self.failure_count = 0 535 | self.error_count = 0 536 | self.verbosity = verbosity 537 | 538 | # result is a list of result in 4 tuple 539 | # ( 540 | # result code (0: success; 1: fail; 2: error), 541 | # TestCase object, 542 | # Test output (byte string), 543 | # stack trace, 544 | # ) 545 | self.result = [] 546 | 547 | 548 | def startTest(self, test): 549 | TestResult.startTest(self, test) 550 | # just one buffer for both stdout and stderr 551 | stdout_redirector.fp = self.outputBuffer 552 | stderr_redirector.fp = self.outputBuffer 553 | self.stdout0 = sys.stdout 554 | self.stderr0 = sys.stderr 555 | sys.stdout = stdout_redirector 556 | sys.stderr = stderr_redirector 557 | 558 | 559 | def complete_output(self): 560 | """ 561 | Disconnect output redirection and return buffer. 562 | Safe to call multiple times. 563 | """ 564 | if self.stdout0: 565 | sys.stdout = self.stdout0 566 | sys.stderr = self.stderr0 567 | self.stdout0 = None 568 | self.stderr0 = None 569 | return self.outputBuffer.getvalue() 570 | 571 | 572 | def stopTest(self, test): 573 | # Usually one of addSuccess, addError or addFailure would have been called. 574 | # But there are some path in unittest that would bypass this. 575 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 576 | self.complete_output() 577 | 578 | 579 | def addSuccess(self, test): 580 | self.success_count += 1 581 | TestResult.addSuccess(self, test) 582 | output = self.complete_output() 583 | self.result.append((0, test, output, '')) 584 | if self.verbosity > 1: 585 | sys.stderr.write('ok ') 586 | sys.stderr.write(str(test)) 587 | sys.stderr.write('\n') 588 | else: 589 | sys.stderr.write('.') 590 | 591 | def addError(self, test, err): 592 | self.error_count += 1 593 | TestResult.addError(self, test, err) 594 | _, _exc_str = self.errors[-1] 595 | output = self.complete_output() 596 | self.result.append((2, test, output, _exc_str)) 597 | if self.verbosity > 1: 598 | sys.stderr.write('E ') 599 | sys.stderr.write(str(test)) 600 | sys.stderr.write('\n') 601 | else: 602 | sys.stderr.write('E') 603 | 604 | def addFailure(self, test, err): 605 | self.failure_count += 1 606 | TestResult.addFailure(self, test, err) 607 | _, _exc_str = self.failures[-1] 608 | output = self.complete_output() 609 | self.result.append((1, test, output, _exc_str)) 610 | if self.verbosity > 1: 611 | sys.stderr.write('F ') 612 | sys.stderr.write(str(test)) 613 | sys.stderr.write('\n') 614 | else: 615 | sys.stderr.write('F') 616 | 617 | 618 | class HTMLTestRunner(Template_mixin): 619 | """ 620 | """ 621 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 622 | self.stream = stream 623 | self.verbosity = verbosity 624 | if title is None: 625 | self.title = self.DEFAULT_TITLE 626 | else: 627 | self.title = title 628 | if description is None: 629 | self.description = self.DEFAULT_DESCRIPTION 630 | else: 631 | self.description = description 632 | 633 | self.startTime = datetime.datetime.now() 634 | 635 | 636 | def run(self, test): 637 | "Run the given test case or test suite." 638 | result = _TestResult(self.verbosity) 639 | test(result) 640 | self.stopTime = datetime.datetime.now() 641 | self.generateReport(test, result) 642 | print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) 643 | return result 644 | 645 | 646 | def sortResult(self, result_list): 647 | # unittest does not seems to run in any particular order. 648 | # Here at least we want to group them together by class. 649 | rmap = {} 650 | classes = [] 651 | for n,t,o,e in result_list: 652 | cls = t.__class__ 653 | if not rmap.has_key(cls): 654 | rmap[cls] = [] 655 | classes.append(cls) 656 | rmap[cls].append((n,t,o,e)) 657 | r = [(cls, rmap[cls]) for cls in classes] 658 | return r 659 | 660 | 661 | def getReportAttributes(self, result): 662 | """ 663 | Return report attributes as a list of (name, value). 664 | Override this to add custom attributes. 665 | """ 666 | startTime = str(self.startTime)[:19] 667 | duration = str(self.stopTime - self.startTime) 668 | status = [] 669 | if result.success_count: status.append('Pass %s' % result.success_count) 670 | if result.failure_count: status.append('Failure %s' % result.failure_count) 671 | if result.error_count: status.append('Error %s' % result.error_count ) 672 | if status: 673 | status = ' '.join(status) 674 | else: 675 | status = 'none' 676 | return [ 677 | ('Start Time', startTime), 678 | ('Duration', duration), 679 | ('Status', status), 680 | ] 681 | 682 | 683 | def generateReport(self, test, result): 684 | report_attrs = self.getReportAttributes(result) 685 | generator = 'HTMLTestRunner %s' % __version__ 686 | stylesheet = self._generate_stylesheet() 687 | heading = self._generate_heading(report_attrs) 688 | report = self._generate_report(result) 689 | ending = self._generate_ending() 690 | output = self.HTML_TMPL % dict( 691 | title = saxutils.escape(self.title), 692 | generator = generator, 693 | stylesheet = stylesheet, 694 | heading = heading, 695 | report = report, 696 | ending = ending, 697 | ) 698 | self.stream.write(output.encode('utf8')) 699 | 700 | 701 | def _generate_stylesheet(self): 702 | return self.STYLESHEET_TMPL 703 | 704 | 705 | def _generate_heading(self, report_attrs): 706 | a_lines = [] 707 | for name, value in report_attrs: 708 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 709 | name = saxutils.escape(name), 710 | value = saxutils.escape(value), 711 | ) 712 | a_lines.append(line) 713 | heading = self.HEADING_TMPL % dict( 714 | title = saxutils.escape(self.title), 715 | parameters = ''.join(a_lines), 716 | description = saxutils.escape(self.description), 717 | ) 718 | return heading 719 | 720 | 721 | def _generate_report(self, result): 722 | rows = [] 723 | sortedResult = self.sortResult(result.result) 724 | for cid, (cls, cls_results) in enumerate(sortedResult): 725 | # subtotal for a class 726 | np = nf = ne = 0 727 | for n,t,o,e in cls_results: 728 | if n == 0: np += 1 729 | elif n == 1: nf += 1 730 | else: ne += 1 731 | 732 | # format class description 733 | if cls.__module__ == "__main__": 734 | name = cls.__name__ 735 | else: 736 | name = "%s.%s" % (cls.__module__, cls.__name__) 737 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 738 | desc = doc and '%s: %s' % (name, doc) or name 739 | 740 | row = self.REPORT_CLASS_TMPL % dict( 741 | style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 742 | desc = desc, 743 | count = np+nf+ne, 744 | Pass = np, 745 | fail = nf, 746 | error = ne, 747 | cid = 'c%s' % (cid+1), 748 | ) 749 | rows.append(row) 750 | 751 | for tid, (n,t,o,e) in enumerate(cls_results): 752 | self._generate_report_test(rows, cid, tid, n, t, o, e) 753 | 754 | report = self.REPORT_TMPL % dict( 755 | test_list = ''.join(rows), 756 | count = str(result.success_count+result.failure_count+result.error_count), 757 | Pass = str(result.success_count), 758 | fail = str(result.failure_count), 759 | error = str(result.error_count), 760 | ) 761 | return report 762 | 763 | 764 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 765 | # e.g. 'pt1.1', 'ft1.1', etc 766 | has_output = bool(o or e) 767 | tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 768 | name = t.id().split('.')[-1] 769 | doc = t.shortDescription() or "" 770 | desc = doc and ('%s: %s' % (name, doc)) or name 771 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 772 | 773 | # o and e should be byte string because they are collected from stdout and stderr? 774 | if isinstance(o,str): 775 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 776 | # uo = unicode(o.encode('string_escape')) 777 | uo = o.decode('latin-1') 778 | else: 779 | uo = o 780 | if isinstance(e,str): 781 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 782 | # ue = unicode(e.encode('string_escape')) 783 | ue = e.decode('latin-1') 784 | else: 785 | ue = e 786 | 787 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 788 | id = tid, 789 | output = saxutils.escape(uo+ue), 790 | ) 791 | 792 | row = tmpl % dict( 793 | tid = tid, 794 | Class = (n == 0 and 'hiddenRow' or 'none'), 795 | style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 796 | desc = desc, 797 | script = script, 798 | status = self.STATUS[n], 799 | ) 800 | rows.append(row) 801 | if not has_output: 802 | return 803 | 804 | def _generate_ending(self): 805 | return self.ENDING_TMPL 806 | 807 | 808 | ############################################################################## 809 | # Facilities for running tests from the command line 810 | ############################################################################## 811 | 812 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 813 | # build our own launcher to support more specific command line 814 | # parameters like test title, CSS, etc. 815 | class TestProgram(unittest.TestProgram): 816 | """ 817 | A variation of the unittest.TestProgram. Please refer to the base 818 | class for command line parameters. 819 | """ 820 | def runTests(self): 821 | # Pick HTMLTestRunner as the default test runner. 822 | # base class's testRunner parameter is not useful because it means 823 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 824 | if self.testRunner is None: 825 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 826 | unittest.TestProgram.runTests(self) 827 | 828 | main = TestProgram 829 | 830 | ############################################################################## 831 | # Executing this module from the command line 832 | ############################################################################## 833 | 834 | if __name__ == "__main__": 835 | main(module=None) 836 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | HTMLTestRunner is an extension to the Python standard library's unittest module. 2 | It generates easy to use HTML test reports. HTMLTestRunner is released under a 3 | BSD style license. 4 | 5 | Only a single file module HTMLTestRunner.py is needed to generate your report. 6 | -------------------------------------------------------------------------------- /test_HTMLTestRunner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import StringIO 4 | import sys 5 | import unittest 6 | 7 | import HTMLTestRunner 8 | 9 | # ---------------------------------------------------------------------- 10 | 11 | def safe_unicode(obj, *args): 12 | """ return the unicode representation of obj """ 13 | try: 14 | return unicode(obj, *args) 15 | except UnicodeDecodeError: 16 | # obj is byte string 17 | ascii_text = str(obj).encode('string_escape') 18 | return unicode(ascii_text) 19 | 20 | def safe_str(obj): 21 | """ return the byte string representation of obj """ 22 | try: 23 | return str(obj) 24 | except UnicodeEncodeError: 25 | # obj is unicode 26 | return unicode(obj).encode('unicode_escape') 27 | 28 | # ---------------------------------------------------------------------- 29 | # Sample tests to drive the HTMLTestRunner 30 | 31 | class SampleTest0(unittest.TestCase): 32 | """ A class that passes. 33 | 34 | This simple class has only one test case that passes. 35 | """ 36 | def __init__(self, methodName): 37 | unittest.TestCase.__init__(self, methodName) 38 | 39 | def test_pass_no_output(self): 40 | """ test description 41 | """ 42 | pass 43 | 44 | class SampleTest1(unittest.TestCase): 45 | """ A class that fails. 46 | 47 | This simple class has only one test case that fails. 48 | """ 49 | def test_fail(self): 50 | u""" test description (描述) """ 51 | self.fail() 52 | 53 | class SampleOutputTestBase(unittest.TestCase): 54 | """ Base TestCase. Generates 4 test cases x different content type. """ 55 | def test_1(self): 56 | print self.MESSAGE 57 | def test_2(self): 58 | print >>sys.stderr, self.MESSAGE 59 | def test_3(self): 60 | self.fail(self.MESSAGE) 61 | def test_4(self): 62 | raise RuntimeError(self.MESSAGE) 63 | 64 | class SampleTestBasic(SampleOutputTestBase): 65 | MESSAGE = 'basic test' 66 | 67 | class SampleTestHTML(SampleOutputTestBase): 68 | MESSAGE = 'the message is 5 symbols: <>&"\'\nplus the HTML entity string: [©] on a second line' 69 | 70 | class SampleTestLatin1(SampleOutputTestBase): 71 | MESSAGE = u'the message is áéíóú'.encode('latin-1') 72 | 73 | class SampleTestUnicode(SampleOutputTestBase): 74 | u""" Unicode (統一碼) test """ 75 | MESSAGE = u'the message is \u8563' 76 | # 2006-04-25 Note: Exception would show up as 77 | # AssertionError: 78 | # 79 | # This seems to be limitation of traceback.format_exception() 80 | # Same result in standard unittest. 81 | 82 | # 2011-03-28 Note: I think it is fixed in Python 2.6 83 | def test_pass(self): 84 | u""" A test with Unicode (統一碼) docstring """ 85 | pass 86 | 87 | 88 | # ------------------------------------------------------------------------ 89 | # This is the main test on HTMLTestRunner 90 | 91 | class Test_HTMLTestRunner(unittest.TestCase): 92 | 93 | def test0(self): 94 | self.suite = unittest.TestSuite() 95 | buf = StringIO.StringIO() 96 | runner = HTMLTestRunner.HTMLTestRunner(buf) 97 | runner.run(self.suite) 98 | # didn't blow up? ok. 99 | self.assert_('' in buf.getvalue()) 100 | 101 | def test_main(self): 102 | # Run HTMLTestRunner. Verify the HTML report. 103 | 104 | # suite of TestCases 105 | self.suite = unittest.TestSuite() 106 | self.suite.addTests([ 107 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTest0), 108 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTest1), 109 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestBasic), 110 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestHTML), 111 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestLatin1), 112 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestUnicode), 113 | ]) 114 | 115 | # Invoke TestRunner 116 | buf = StringIO.StringIO() 117 | #runner = unittest.TextTestRunner(buf) #DEBUG: this is the unittest baseline 118 | runner = HTMLTestRunner.HTMLTestRunner( 119 | stream=buf, 120 | title='', 121 | description='This demonstrates the report output by HTMLTestRunner.' 122 | ) 123 | runner.run(self.suite) 124 | 125 | # Define the expected output sequence. This is imperfect but should 126 | # give a good sense of the well being of the test. 127 | EXPECTED = u""" 128 | Demo Test 129 | 130 | >SampleTest0: 131 | 132 | >SampleTest1: 133 | 134 | >SampleTestBasic 135 | >test_1< 136 | pass 137 | basic test 138 | 139 | >test_2< 140 | pass 141 | basic test 142 | 143 | >test_3< 144 | fail 145 | AssertionError: basic test 146 | 147 | >test_4< 148 | error 149 | RuntimeError: basic test 150 | 151 | 152 | >SampleTestHTML 153 | >test_1< 154 | pass 155 | the message is 5 symbols: <>&"' 156 | plus the HTML entity string: [&copy;] on a second line 157 | 158 | >test_2< 159 | pass 160 | the message is 5 symbols: <>&"' 161 | plus the HTML entity string: [&copy;] on a second line 162 | 163 | >test_3< 164 | fail 165 | AssertionError: the message is 5 symbols: <>&"' 166 | plus the HTML entity string: [&copy;] on a second line 167 | 168 | >test_4< 169 | error 170 | RuntimeError: the message is 5 symbols: <>&"' 171 | plus the HTML entity string: [&copy;] on a second line 172 | 173 | 174 | >SampleTestLatin1 175 | >test_1< 176 | pass 177 | the message is áéíóú 178 | 179 | >test_2< 180 | pass 181 | the message is áéíóú 182 | 183 | >test_3< 184 | fail 185 | AssertionError: the message is áéíóú 186 | 187 | >test_4< 188 | error 189 | RuntimeError: the message is áéíóú 190 | 191 | 192 | >SampleTestUnicode 193 | >test_1< 194 | pass 195 | the message is \u8563 196 | 197 | >test_2< 198 | pass 199 | the message is \u8563 200 | 201 | >test_3< 202 | fail 203 | AssertionError: 204 | 205 | >test_4< 206 | error 207 | RuntimeError: 208 | 209 | Total 210 | >19< 211 | >10< 212 | >5< 213 | >4< 214 | 215 | """ 216 | # check out the output 217 | byte_output = buf.getvalue() 218 | # output the main test output for debugging & demo 219 | print byte_output 220 | # HTMLTestRunner pumps UTF-8 output 221 | output = byte_output.decode('utf-8') 222 | self._checkoutput(output,EXPECTED) 223 | 224 | 225 | def _checkoutput(self,output,EXPECTED): 226 | i = 0 227 | for lineno, p in enumerate(EXPECTED.splitlines()): 228 | if not p: 229 | continue 230 | j = output.find(p,i) 231 | if j < 0: 232 | self.fail(safe_str('Pattern not found lineno %s: "%s"' % (lineno+1,p))) 233 | i = j + len(p) 234 | 235 | 236 | 237 | 238 | ############################################################################## 239 | # Executing this module from the command line 240 | ############################################################################## 241 | 242 | import unittest 243 | if __name__ == "__main__": 244 | if len(sys.argv) > 1: 245 | argv = sys.argv 246 | else: 247 | argv=['test_HTMLTestRunner.py', 'Test_HTMLTestRunner'] 248 | unittest.main(argv=argv) 249 | # Testing HTMLTestRunner with HTMLTestRunner would work. But instead 250 | # we will use standard library's TextTestRunner to reduce the nesting 251 | # that may confuse people. 252 | #HTMLTestRunner.main(argv=argv) 253 | 254 | --------------------------------------------------------------------------------