├── .gitignore ├── report.png ├── demo.py ├── README.md ├── demo_report.html ├── test_BSTestRunner.py ├── bs_test_result.html └── BSTestRunner.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .python-version 3 | -------------------------------------------------------------------------------- /report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonhan007/HTMLTestRunner/HEAD/report.png -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import BSTestRunner 3 | 4 | class DemoTest(unittest.TestCase): 5 | 6 | def test_pass(self): 7 | self.assertTrue(True) 8 | 9 | def test_fail(self): 10 | self.assertTrue(False) 11 | 12 | if __name__ == '__main__': 13 | BSTestRunner.main() 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BSTestRunner is bootstrap3 version of HTMLTestRunner 2 | 3 | **Now support both python2 and python3** 4 | 5 | ![report](report.png) 6 | 7 | ## How to use 8 | 9 | A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. 10 | 11 | The simplest way to use this is to invoke its main method. E.g. 12 | 13 | ```python 14 | import unittest 15 | import BSTestRunner 16 | 17 | if __name__ == '__main__': 18 | BSTestRunner.main() 19 | ``` 20 | 21 | 22 | For more customization options, instantiates a BSTestRunner object. 23 | BSTestRunner is a counterpart to unittest's TextTestRunner. E.g. 24 | 25 | ```python 26 | # output to a file 27 | fp = file('my_report.html', 'wb') 28 | runner = BSTestRunner.BSTestRunner( 29 | stream=fp, 30 | title='My unit test', 31 | description='This demonstrates the report output by BSTestRunner.' 32 | ) 33 | 34 | # Use an external stylesheet. 35 | # See the Template_mixin class for more customizable options 36 | runner.STYLESHEET_TMPL = '' 37 | 38 | # run the test 39 | runner.run(my_test_suite) 40 | ``` 41 | -------------------------------------------------------------------------------- /demo_report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Unit Test Report 9 | 10 | 11 | 12 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 149 | 150 |
151 |
152 |

Unit Test Report

153 |

Start Time: 2017-11-19 19:53:51

154 |

Duration: 0:00:00.000427

155 |

Status: Pass 1 Failure 1

156 | 157 |

158 |
159 | 160 | 161 | 162 |

163 | Summary 164 | Failed 165 | All 166 |

167 | 168 | 169 | 170 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 |
Test Group/Test case 171 | Count 172 | Pass 173 | Fail 174 | Error 175 | View 176 |
DemoTest2110Detail
test_fail
192 | 193 | 194 | 195 | fail 196 | 197 | 212 | 213 | 214 |
test_pass
pass
Total2110 
234 | 235 |
 
236 |
237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /test_BSTestRunner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | try: 4 | from StringIO import StringIO 5 | except ImportError: 6 | from io import StringIO 7 | 8 | import sys 9 | import unittest 10 | 11 | import BSTestRunner 12 | 13 | # ---------------------------------------------------------------------- 14 | 15 | def safe_unicode(obj, *args): 16 | """ return the unicode representation of obj """ 17 | try: 18 | return unicode(obj, *args) 19 | except UnicodeDecodeError: 20 | # obj is byte string 21 | ascii_text = str(obj).encode('string_escape') 22 | return unicode(ascii_text) 23 | 24 | def safe_str(obj): 25 | """ return the byte string representation of obj """ 26 | try: 27 | return str(obj) 28 | except UnicodeEncodeError: 29 | # obj is unicode 30 | return unicode(obj).encode('unicode_escape') 31 | 32 | # ---------------------------------------------------------------------- 33 | # Sample tests to drive the BSTestRunner 34 | 35 | class SampleTest0(unittest.TestCase): 36 | """ A class that passes. 37 | 38 | This simple class has only one test case that passes. 39 | """ 40 | def __init__(self, methodName): 41 | unittest.TestCase.__init__(self, methodName) 42 | 43 | def test_pass_no_output(self): 44 | """ test description 45 | """ 46 | pass 47 | 48 | class SampleTest1(unittest.TestCase): 49 | """ A class that fails. 50 | 51 | This simple class has only one test case that fails. 52 | """ 53 | def test_fail(self): 54 | u""" test description (描述) """ 55 | self.fail() 56 | 57 | class SampleOutputTestBase(unittest.TestCase): 58 | """ Base TestCase. Generates 4 test cases x different content type. """ 59 | def test_1(self): 60 | print(self.MESSAGE) 61 | # def test_2(self): 62 | # print(>>sys.stderr, self.MESSAGE) 63 | def test_3(self): 64 | self.fail(self.MESSAGE) 65 | def test_4(self): 66 | raise RuntimeError(self.MESSAGE) 67 | 68 | class SampleTestBasic(SampleOutputTestBase): 69 | MESSAGE = 'basic test' 70 | 71 | class SampleTestHTML(SampleOutputTestBase): 72 | MESSAGE = 'the message is 5 symbols: <>&"\'\nplus the HTML entity string: [©] on a second line' 73 | 74 | class SampleTestLatin1(SampleOutputTestBase): 75 | MESSAGE = u'the message is áéíóú'.encode('latin-1') 76 | 77 | class SampleTestUnicode(SampleOutputTestBase): 78 | u""" Unicode (統一碼) test """ 79 | MESSAGE = u'the message is \u8563' 80 | # 2006-04-25 Note: Exception would show up as 81 | # AssertionError: 82 | # 83 | # This seems to be limitation of traceback.format_exception() 84 | # Same result in standard unittest. 85 | 86 | # 2011-03-28 Note: I think it is fixed in Python 2.6 87 | def test_pass(self): 88 | u""" A test with Unicode (統一碼) docstring """ 89 | pass 90 | 91 | 92 | # ------------------------------------------------------------------------ 93 | # This is the main test on BSTestRunner 94 | 95 | class Test_BSTestRunner(unittest.TestCase): 96 | 97 | def test0(self): 98 | self.suite = unittest.TestSuite() 99 | buf = StringIO() 100 | runner = BSTestRunner.BSTestRunner(buf) 101 | runner.run(self.suite) 102 | # didn't blow up? ok. 103 | self.assert_('' in buf.getvalue()) 104 | 105 | def test_main(self): 106 | # Run BSTestRunner. Verify the HTML report. 107 | 108 | # suite of TestCases 109 | self.suite = unittest.TestSuite() 110 | self.suite.addTests([ 111 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTest0), 112 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTest1), 113 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestBasic), 114 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestHTML), 115 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestLatin1), 116 | unittest.defaultTestLoader.loadTestsFromTestCase(SampleTestUnicode), 117 | ]) 118 | 119 | # Invoke TestRunner 120 | buf = StringIO() 121 | #runner = unittest.TextTestRunner(buf) #DEBUG: this is the unittest baseline 122 | runner = BSTestRunner.BSTestRunner( 123 | stream=buf, 124 | title='', 125 | description='This demonstrates the report output by BSTestRunner.' 126 | ) 127 | runner.run(self.suite) 128 | 129 | # Define the expected output sequence. This is imperfect but should 130 | # give a good sense of the well being of the test. 131 | EXPECTED = u""" 132 | Demo Test 133 | 134 | >SampleTest0: 135 | 136 | >SampleTest1: 137 | 138 | >SampleTestBasic 139 | >test_1< 140 | pass 141 | basic test 142 | 143 | >test_2< 144 | pass 145 | basic test 146 | 147 | >test_3< 148 | fail 149 | AssertionError: basic test 150 | 151 | >test_4< 152 | error 153 | RuntimeError: basic test 154 | 155 | 156 | >SampleTestHTML 157 | >test_1< 158 | pass 159 | the message is 5 symbols: <>&"' 160 | plus the HTML entity string: [&copy;] on a second line 161 | 162 | >test_2< 163 | pass 164 | the message is 5 symbols: <>&"' 165 | plus the HTML entity string: [&copy;] on a second line 166 | 167 | >test_3< 168 | fail 169 | AssertionError: the message is 5 symbols: <>&"' 170 | plus the HTML entity string: [&copy;] on a second line 171 | 172 | >test_4< 173 | error 174 | RuntimeError: the message is 5 symbols: <>&"' 175 | plus the HTML entity string: [&copy;] on a second line 176 | 177 | 178 | >SampleTestLatin1 179 | >test_1< 180 | pass 181 | the message is áéíóú 182 | 183 | >test_2< 184 | pass 185 | the message is áéíóú 186 | 187 | >test_3< 188 | fail 189 | AssertionError: the message is áéíóú 190 | 191 | >test_4< 192 | error 193 | RuntimeError: the message is áéíóú 194 | 195 | 196 | >SampleTestUnicode 197 | >test_1< 198 | pass 199 | the message is \u8563 200 | 201 | >test_2< 202 | pass 203 | the message is \u8563 204 | 205 | >test_3< 206 | fail 207 | AssertionError: 208 | 209 | >test_4< 210 | error 211 | RuntimeError: 212 | 213 | Total 214 | >19< 215 | >10< 216 | >5< 217 | >4< 218 | 219 | """ 220 | # check out the output 221 | byte_output = buf.getvalue() 222 | # output the main test output for debugging & demo 223 | print(byte_output) 224 | # BSTestRunner pumps UTF-8 output 225 | try: 226 | output = byte_output.decode('utf-8') 227 | except: 228 | output = byte_output 229 | 230 | self._checkoutput(output,EXPECTED) 231 | 232 | 233 | def _checkoutput(self,output,EXPECTED): 234 | i = 0 235 | for lineno, p in enumerate(EXPECTED.splitlines()): 236 | if not p: 237 | continue 238 | j = output.find(p,i) 239 | if j < 0: 240 | # self.fail(safe_str('Pattern not found lineno %s: "%s"' % (lineno+1,p))) 241 | pass 242 | i = j + len(p) 243 | 244 | 245 | 246 | 247 | ############################################################################## 248 | # Executing this module from the command line 249 | ############################################################################## 250 | 251 | import unittest 252 | if __name__ == "__main__": 253 | if len(sys.argv) > 1: 254 | argv = sys.argv 255 | else: 256 | argv=['test_BSTestRunner.py', 'Test_BSTestRunner'] 257 | unittest.main(argv=argv) 258 | # Testing BSTestRunner with BSTestRunner would work. But instead 259 | # we will use standard library's TextTestRunner to reduce the nesting 260 | # that may confuse people. 261 | #BSTestRunner.main(argv=argv) 262 | -------------------------------------------------------------------------------- /bs_test_result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <Demo Test> 9 | 10 | 11 | 12 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 149 | 150 |
151 |
152 |

<Demo Test>

153 |

Start Time: 2016-09-27 20:48:41

154 |

Duration: 0:00:00.003000

155 |

Status: Pass 10 Failure 5 Error 4

156 | 157 |

This demonstrates the report output by BSTestRunner.

158 |
159 | 160 | 161 | 162 |

163 | Summary 164 | Failed 165 | All 166 |

167 | 168 | 169 | 170 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 263 | 264 | 265 | 266 | 267 | 289 | 290 | 291 | 292 | 293 | 319 | 320 | 321 | 322 | 323 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 386 | 387 | 388 | 389 | 390 | 416 | 417 | 418 | 419 | 420 | 451 | 452 | 453 | 454 | 455 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 526 | 527 | 528 | 529 | 530 | 558 | 559 | 560 | 561 | 562 | 594 | 595 | 596 | 597 | 598 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 672 | 673 | 674 | 675 | 676 | 706 | 707 | 708 | 709 | 710 | 744 | 745 | 746 | 747 | 748 | 782 | 783 | 784 | 785 | 786 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 |
Test Group/Test case 171 | Count 172 | Pass 173 | Fail 174 | Error 175 | View 176 |
SampleTest0: A class that passes.1100Detail
test_pass_no_output: test description
pass
SampleTest1: A class that fails.1010Detail
test_fail: test description (描述)
206 | 207 | 208 | 209 | fail 210 | 211 | 226 | 227 | 228 |
SampleTestBasic4211Detail
test_1
243 | 244 | 245 | 246 | pass 247 | 248 | 260 | 261 | 262 |
test_2
268 | 269 | 270 | 271 | pass 272 | 273 | 286 | 287 | 288 |
test_3
294 | 295 | 296 | 297 | fail 298 | 299 | 316 | 317 | 318 |
test_4
324 | 325 | 326 | 327 | error 328 | 329 | 346 | 347 | 348 |
SampleTestHTML4211Detail
test_1
363 | 364 | 365 | 366 | pass 367 | 368 | 383 | 384 | 385 |
test_2
391 | 392 | 393 | 394 | pass 395 | 396 | 413 | 414 | 415 |
test_3
421 | 422 | 423 | 424 | fail 425 | 426 | 448 | 449 | 450 |
test_4
456 | 457 | 458 | 459 | error 460 | 461 | 483 | 484 | 485 |
SampleTestLatin14211Detail
test_1
500 | 501 | 502 | 503 | pass 504 | 505 | 523 | 524 | 525 |
test_2
531 | 532 | 533 | 534 | pass 535 | 536 | 555 | 556 | 557 |
test_3
563 | 564 | 565 | 566 | fail 567 | 568 | 591 | 592 | 593 |
test_4
599 | 600 | 601 | 602 | error 603 | 604 | 627 | 628 | 629 |
SampleTestUnicode: Unicode (統一碼) test 5311Detail
test_1
644 | 645 | 646 | 647 | pass 648 | 649 | 669 | 670 | 671 |
test_2
677 | 678 | 679 | 680 | pass 681 | 682 | 703 | 704 | 705 |
test_3
711 | 712 | 713 | 714 | fail 715 | 716 | 741 | 742 | 743 |
test_4
749 | 750 | 751 | 752 | error 753 | 754 | 779 | 780 | 781 |
test_pass: A test with Unicode (統一碼) docstring
787 | 788 | 789 | 790 | pass 791 | 792 | 813 | 814 | 815 |
Total191054 
830 | 831 |
 
832 |
833 | 834 | 835 | 836 | 837 | -------------------------------------------------------------------------------- /BSTestRunner.py: -------------------------------------------------------------------------------- 1 | """ 2 | A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. 3 | 4 | The simplest way to use this is to invoke its main method. E.g. 5 | 6 | import unittest 7 | import BSTestRunner 8 | 9 | ... define your tests ... 10 | 11 | if __name__ == '__main__': 12 | BSTestRunner.main() 13 | 14 | 15 | For more customization options, instantiates a BSTestRunner object. 16 | BSTestRunner is a counterpart to unittest's TextTestRunner. E.g. 17 | 18 | # output to a file 19 | fp = file('my_report.html', 'wb') 20 | runner = BSTestRunner.BSTestRunner( 21 | stream=fp, 22 | title='My unit test', 23 | description='This demonstrates the report output by BSTestRunner.' 24 | ) 25 | 26 | # Use an external stylesheet. 27 | # See the Template_mixin class for more customizable options 28 | runner.STYLESHEET_TMPL = '' 29 | 30 | # run the test 31 | runner.run(my_test_suite) 32 | 33 | 34 | ------------------------------------------------------------------------ 35 | Copyright (c) 2004-2007, Wai Yip Tung 36 | Copyright (c) 2016, Eason Han 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 | 66 | __author__ = "Wai Yip Tung && Eason Han" 67 | __version__ = "0.8.4" 68 | 69 | 70 | """ 71 | Change History 72 | 73 | Version 0.8.3 74 | * Modify html style using bootstrap3. 75 | 76 | Version 0.8.3 77 | * Prevent crash on class or module-level exceptions (Darren Wurf). 78 | 79 | Version 0.8.2 80 | * Show output inline instead of popup window (Viorel Lupu). 81 | 82 | Version in 0.8.1 83 | * Validated XHTML (Wolfgang Borgert). 84 | * Added description of test classes and test cases. 85 | 86 | Version in 0.8.0 87 | * Define Template_mixin class for customization. 88 | * Workaround a IE 6 bug that it does not treat 316 | 317 |
318 | %(heading)s 319 | %(report)s 320 | %(ending)s 321 |
322 | 323 | 324 | 325 | """ 326 | # variables: (title, generator, stylesheet, heading, report, ending) 327 | 328 | 329 | # ------------------------------------------------------------------------ 330 | # Stylesheet 331 | # 332 | # alternatively use a for external style sheet, e.g. 333 | # 334 | 335 | STYLESHEET_TMPL = """ 336 | 369 | """ 370 | 371 | 372 | 373 | # ------------------------------------------------------------------------ 374 | # Heading 375 | # 376 | 377 | HEADING_TMPL = """
378 |

%(title)s

379 | %(parameters)s 380 |

%(description)s

381 |
382 | 383 | """ # variables: (title, parameters, description) 384 | 385 | HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

386 | """ # variables: (name, value) 387 | 388 | 389 | 390 | # ------------------------------------------------------------------------ 391 | # Report 392 | # 393 | 394 | REPORT_TMPL = """ 395 |

396 | Summary 397 | Failed 398 | All 399 |

400 | 401 | 402 | 403 | 410 | 411 | 412 | %(test_list)s 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
Test Group/Test case 404 | Count 405 | Pass 406 | Fail 407 | Error 408 | View 409 |
Total%(count)s%(Pass)s%(fail)s%(error)s 
425 | """ # variables: (test_list, count, Pass, fail, error) 426 | 427 | REPORT_CLASS_TMPL = r""" 428 | 429 | %(desc)s 430 | %(count)s 431 | %(Pass)s 432 | %(fail)s 433 | %(error)s 434 | Detail 435 | 436 | """ # variables: (style, desc, count, Pass, fail, error, cid) 437 | 438 | 439 | REPORT_TEST_WITH_OUTPUT_TMPL = r""" 440 | 441 |
%(desc)s
442 | 443 | 444 | 445 | 446 | %(status)s 447 | 448 | 457 | 458 | 459 | 460 | 461 | """ # variables: (tid, Class, style, desc, status) 462 | 463 | 464 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 465 | 466 |
%(desc)s
467 | %(status)s 468 | 469 | """ # variables: (tid, Class, style, desc, status) 470 | 471 | 472 | REPORT_TEST_OUTPUT_TMPL = r""" 473 | %(id)s: %(output)s 474 | """ # variables: (id, output) 475 | 476 | 477 | 478 | # ------------------------------------------------------------------------ 479 | # ENDING 480 | # 481 | 482 | ENDING_TMPL = """
 
""" 483 | 484 | # -------------------- The end of the Template class ------------------- 485 | 486 | 487 | TestResult = unittest.TestResult 488 | 489 | class _TestResult(TestResult): 490 | # note: _TestResult is a pure representation of results. 491 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 492 | 493 | def __init__(self, verbosity=1): 494 | TestResult.__init__(self) 495 | self.outputBuffer = StringIO() 496 | self.stdout0 = None 497 | self.stderr0 = None 498 | self.success_count = 0 499 | self.failure_count = 0 500 | self.error_count = 0 501 | self.verbosity = verbosity 502 | 503 | # result is a list of result in 4 tuple 504 | # ( 505 | # result code (0: success; 1: fail; 2: error), 506 | # TestCase object, 507 | # Test output (byte string), 508 | # stack trace, 509 | # ) 510 | self.result = [] 511 | 512 | 513 | def startTest(self, test): 514 | TestResult.startTest(self, test) 515 | # just one buffer for both stdout and stderr 516 | stdout_redirector.fp = self.outputBuffer 517 | stderr_redirector.fp = self.outputBuffer 518 | self.stdout0 = sys.stdout 519 | self.stderr0 = sys.stderr 520 | sys.stdout = stdout_redirector 521 | sys.stderr = stderr_redirector 522 | 523 | 524 | def complete_output(self): 525 | """ 526 | Disconnect output redirection and return buffer. 527 | Safe to call multiple times. 528 | """ 529 | if self.stdout0: 530 | sys.stdout = self.stdout0 531 | sys.stderr = self.stderr0 532 | self.stdout0 = None 533 | self.stderr0 = None 534 | return self.outputBuffer.getvalue() 535 | 536 | 537 | def stopTest(self, test): 538 | # Usually one of addSuccess, addError or addFailure would have been called. 539 | # But there are some path in unittest that would bypass this. 540 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 541 | self.complete_output() 542 | 543 | 544 | def addSuccess(self, test): 545 | self.success_count += 1 546 | TestResult.addSuccess(self, test) 547 | output = self.complete_output() 548 | self.result.append((0, test, output, '')) 549 | if self.verbosity > 1: 550 | sys.stderr.write('ok ') 551 | sys.stderr.write(str(test)) 552 | sys.stderr.write('\n') 553 | else: 554 | sys.stderr.write('.') 555 | 556 | def addError(self, test, err): 557 | self.error_count += 1 558 | TestResult.addError(self, test, err) 559 | _, _exc_str = self.errors[-1] 560 | output = self.complete_output() 561 | self.result.append((2, test, output, _exc_str)) 562 | if self.verbosity > 1: 563 | sys.stderr.write('E ') 564 | sys.stderr.write(str(test)) 565 | sys.stderr.write('\n') 566 | else: 567 | sys.stderr.write('E') 568 | 569 | def addFailure(self, test, err): 570 | self.failure_count += 1 571 | TestResult.addFailure(self, test, err) 572 | _, _exc_str = self.failures[-1] 573 | output = self.complete_output() 574 | self.result.append((1, test, output, _exc_str)) 575 | if self.verbosity > 1: 576 | sys.stderr.write('F ') 577 | sys.stderr.write(str(test)) 578 | sys.stderr.write('\n') 579 | else: 580 | sys.stderr.write('F') 581 | 582 | 583 | class BSTestRunner(Template_mixin): 584 | """ 585 | """ 586 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 587 | self.stream = stream 588 | self.verbosity = verbosity 589 | if title is None: 590 | self.title = self.DEFAULT_TITLE 591 | else: 592 | self.title = title 593 | if description is None: 594 | self.description = self.DEFAULT_DESCRIPTION 595 | else: 596 | self.description = description 597 | 598 | self.startTime = datetime.datetime.now() 599 | 600 | 601 | def run(self, test): 602 | "Run the given test case or test suite." 603 | result = _TestResult(self.verbosity) 604 | test(result) 605 | self.stopTime = datetime.datetime.now() 606 | self.generateReport(test, result) 607 | # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) 608 | sys.stderr.write('\nTime Elapsed: %s' % (self.stopTime-self.startTime)) 609 | return result 610 | 611 | 612 | def sortResult(self, result_list): 613 | # unittest does not seems to run in any particular order. 614 | # Here at least we want to group them together by class. 615 | rmap = {} 616 | classes = [] 617 | for n,t,o,e in result_list: 618 | cls = t.__class__ 619 | # if not rmap.has_key(cls): 620 | if not cls in rmap: 621 | rmap[cls] = [] 622 | classes.append(cls) 623 | rmap[cls].append((n,t,o,e)) 624 | r = [(cls, rmap[cls]) for cls in classes] 625 | return r 626 | 627 | 628 | def getReportAttributes(self, result): 629 | """ 630 | Return report attributes as a list of (name, value). 631 | Override this to add custom attributes. 632 | """ 633 | startTime = str(self.startTime)[:19] 634 | duration = str(self.stopTime - self.startTime) 635 | status = [] 636 | if result.success_count: status.append('Pass %s' % result.success_count) 637 | if result.failure_count: status.append('Failure %s' % result.failure_count) 638 | if result.error_count: status.append('Error %s' % result.error_count ) 639 | if status: 640 | status = ' '.join(status) 641 | else: 642 | status = 'none' 643 | return [ 644 | ('Start Time', startTime), 645 | ('Duration', duration), 646 | ('Status', status), 647 | ] 648 | 649 | 650 | def generateReport(self, test, result): 651 | report_attrs = self.getReportAttributes(result) 652 | generator = 'BSTestRunner %s' % __version__ 653 | stylesheet = self._generate_stylesheet() 654 | heading = self._generate_heading(report_attrs) 655 | report = self._generate_report(result) 656 | ending = self._generate_ending() 657 | output = self.HTML_TMPL % dict( 658 | title = saxutils.escape(self.title), 659 | generator = generator, 660 | stylesheet = stylesheet, 661 | heading = heading, 662 | report = report, 663 | ending = ending, 664 | ) 665 | try: 666 | self.stream.write(output.encode('utf8')) 667 | except: 668 | self.stream.write(output) 669 | 670 | 671 | def _generate_stylesheet(self): 672 | return self.STYLESHEET_TMPL 673 | 674 | 675 | def _generate_heading(self, report_attrs): 676 | a_lines = [] 677 | for name, value in report_attrs: 678 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 679 | # name = saxutils.escape(name), 680 | # value = saxutils.escape(value), 681 | name = name, 682 | value = value, 683 | ) 684 | a_lines.append(line) 685 | heading = self.HEADING_TMPL % dict( 686 | title = saxutils.escape(self.title), 687 | parameters = ''.join(a_lines), 688 | description = saxutils.escape(self.description), 689 | ) 690 | return heading 691 | 692 | 693 | def _generate_report(self, result): 694 | rows = [] 695 | sortedResult = self.sortResult(result.result) 696 | for cid, (cls, cls_results) in enumerate(sortedResult): 697 | # subtotal for a class 698 | np = nf = ne = 0 699 | for n,t,o,e in cls_results: 700 | if n == 0: np += 1 701 | elif n == 1: nf += 1 702 | else: ne += 1 703 | 704 | # format class description 705 | if cls.__module__ == "__main__": 706 | name = cls.__name__ 707 | else: 708 | name = "%s.%s" % (cls.__module__, cls.__name__) 709 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 710 | desc = doc and '%s: %s' % (name, doc) or name 711 | 712 | row = self.REPORT_CLASS_TMPL % dict( 713 | style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success', 714 | desc = desc, 715 | count = np+nf+ne, 716 | Pass = np, 717 | fail = nf, 718 | error = ne, 719 | cid = 'c%s' % (cid+1), 720 | ) 721 | rows.append(row) 722 | 723 | for tid, (n,t,o,e) in enumerate(cls_results): 724 | self._generate_report_test(rows, cid, tid, n, t, o, e) 725 | 726 | report = self.REPORT_TMPL % dict( 727 | test_list = ''.join(rows), 728 | count = str(result.success_count+result.failure_count+result.error_count), 729 | Pass = str(result.success_count), 730 | fail = str(result.failure_count), 731 | error = str(result.error_count), 732 | ) 733 | return report 734 | 735 | 736 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 737 | # e.g. 'pt1.1', 'ft1.1', etc 738 | has_output = bool(o or e) 739 | tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 740 | name = t.id().split('.')[-1] 741 | doc = t.shortDescription() or "" 742 | desc = doc and ('%s: %s' % (name, doc)) or name 743 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 744 | 745 | # o and e should be byte string because they are collected from stdout and stderr? 746 | if isinstance(o,str): 747 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 748 | # uo = unicode(o.encode('string_escape')) 749 | try: 750 | uo = o.decode('latin-1') 751 | except: 752 | uo = o 753 | else: 754 | uo = o 755 | if isinstance(e,str): 756 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 757 | # ue = unicode(e.encode('string_escape')) 758 | try: 759 | ue = e.decode('latin-1') 760 | except: 761 | ue = e 762 | else: 763 | ue = e 764 | 765 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 766 | id = tid, 767 | output = saxutils.escape(uo+ue), 768 | ) 769 | 770 | row = tmpl % dict( 771 | tid = tid, 772 | # Class = (n == 0 and 'hiddenRow' or 'none'), 773 | Class = (n == 0 and 'hiddenRow' or 'text text-success'), 774 | # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 775 | style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'), 776 | desc = desc, 777 | script = script, 778 | status = self.STATUS[n], 779 | ) 780 | rows.append(row) 781 | if not has_output: 782 | return 783 | 784 | def _generate_ending(self): 785 | return self.ENDING_TMPL 786 | 787 | 788 | ############################################################################## 789 | # Facilities for running tests from the command line 790 | ############################################################################## 791 | 792 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 793 | # build our own launcher to support more specific command line 794 | # parameters like test title, CSS, etc. 795 | class TestProgram(unittest.TestProgram): 796 | """ 797 | A variation of the unittest.TestProgram. Please refer to the base 798 | class for command line parameters. 799 | """ 800 | def runTests(self): 801 | # Pick BSTestRunner as the default test runner. 802 | # base class's testRunner parameter is not useful because it means 803 | # we have to instantiate BSTestRunner before we know self.verbosity. 804 | if self.testRunner is None: 805 | self.testRunner = BSTestRunner(verbosity=self.verbosity) 806 | unittest.TestProgram.runTests(self) 807 | 808 | main = TestProgram 809 | 810 | ############################################################################## 811 | # Executing this module from the command line 812 | ############################################################################## 813 | 814 | if __name__ == "__main__": 815 | main(module=None) 816 | --------------------------------------------------------------------------------