About
简单使用
import webbrowser import unittest import HTMLTestRunner import BSTestRunner class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): self.assertTrue('Foo'.isupper()) if __name__ == '__main__': suite = unittest.makeSuite(TestStringMethods) f1 = open('result1.html', 'wb') f2 = open('result2.html', 'wb') HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run( suite) suite = unittest.makeSuite(TestStringMethods) BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite) f1.close() f2.close() webbrowser.open('result1.html') webbrowser.open('result2.html')
-
stream是文件句柄。
-
title是测试报告的title。
-
description是测试报告的描述信息。
这样在本地就生成了result1.html
和result2.html
两个HTML文件:
OK,还是比较完美的,再来一点优化:
优化版
优化其实很简单:
import webbrowser import unittest import HTMLTestRunner import BSTestRunner class TestStringMethods(unittest.TestCase): def test_upper(self): """判断 foo.upper() 是否等于 FOO""" self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): """ 判断 Foo 是否为大写形式 """ self.assertTrue('Foo'.isupper()) if __name__ == '__main__': suite = unittest.makeSuite(TestStringMethods) f1 = open('result1.html', 'wb') f2 = open('result2.html', 'wb') HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run( suite) suite = unittest.makeSuite(TestStringMethods) BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite) f1.close() f2.close() webbrowser.open('result1.html') webbrowser.open('result2.html')
其实就是为每个用例方法添加上注释说明。
Python2.x版本
import webbrowser import unittest import HTMLTestRunner import BSTestRunner class TestStringMethods(unittest.TestCase): def test_upper(self): u"""判断 foo.upper() 是否等于 FOO""" self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): u""" 判断 Foo 是否为大写形式 """ self.assertTrue('Foo'.isupper()) if __name__ == '__main__': suite = unittest.makeSuite(TestStringMethods) f1 = open('result1.html', 'wb') f2 = open('result2.html', 'wb') HTMLTestRunner.HTMLTestRunner( stream=f1, title=u'HTMLTestRunner版本关于upper的测试报告', description=u'判断upper的测试用例执行情况').run(suite) suite = unittest.makeSuite(TestStringMethods) BSTestRunner.BSTestRunner( stream=f2, title=u'BSTestRunner版本关于upper的测试报告', description=u'判断upper的测试用例执行情况').run(suite) f1.close() f2.close() webbrowser.open('result1.html') webbrowser.open('result2.html')
各版本的两文件的源码,保存到指定位置即可。
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 500px; 353 } 354 355 } 356 /* -- report ------------------------------------------------------------------------ */ 357 #show_detail_line { 358 margin-top: 3ex; 359 margin-bottom: 1ex; 360 } 361 #result_table { 362 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.BytesIO() 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 607 class HTMLTestRunner(Template_mixin): 608 """ 609 """ 610 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 611 self.stream = stream 612 self.verbosity = verbosity 613 if title is None: 614 self.title = self.DEFAULT_TITLE 615 else: 616 self.title = title 617 if description is None: 618 self.description = self.DEFAULT_DESCRIPTION 619 else: 620 self.description = description 621 622 self.startTime = datetime.datetime.now() 623 624 625 def run(self, test): 626 "Run the given test case or test suite." 627 result = _TestResult(self.verbosity) 628 test(result) 629 self.stopTime = datetime.datetime.now() 630 self.generateReport(test, result) 631 print(sys.stderr, ' Time Elapsed: %s' % (self.stopTime-self.startTime)) 632 return result 633 634 635 def sortResult(self, result_list): 636 # unittest does not seems to run in any particular order. 637 # Here at least we want to group them together by class. 638 rmap = {} 639 classes = [] 640 for n,t,o,e in result_list: 641 cls = t.__class__ 642 if not cls in rmap: 643 rmap[cls] = [] 644 classes.append(cls) 645 rmap[cls].append((n,t,o,e)) 646 r = [(cls, rmap[cls]) for cls in classes] 647 return r 648 649 650 def getReportAttributes(self, result): 651 """ 652 Return report attributes as a list of (name, value). 653 Override this to add custom attributes. 654 """ 655 startTime = str(self.startTime)[:19] 656 duration = str(self.stopTime - self.startTime) 657 status = [] 658 if result.success_count: status.append('Pass %s' % result.success_count) 659 if result.failure_count: status.append('Failure %s' % result.failure_count) 660 if result.error_count: status.append('Error %s' % result.error_count ) 661 if status: 662 status = ' '.join(status) 663 else: 664 status = 'none' 665 return [ 666 ('Start Time', startTime), 667 ('Duration', duration), 668 ('Status', status), 669 ] 670 671 672 def generateReport(self, test, result): 673 report_attrs = self.getReportAttributes(result) 674 generator = 'HTMLTestRunner %s' % __version__ 675 stylesheet = self._generate_stylesheet() 676 heading = self._generate_heading(report_attrs) 677 report = self._generate_report(result) 678 ending = self._generate_ending() 679 output = self.HTML_TMPL % dict( 680 title = saxutils.escape(self.title), 681 generator = generator, 682 stylesheet = stylesheet, 683 heading = heading, 684 report = report, 685 ending = ending, 686 ) 687 self.stream.write(output.encode('utf8')) 688 689 690 def _generate_stylesheet(self): 691 return self.STYLESHEET_TMPL 692 693 694 def _generate_heading(self, report_attrs): 695 a_lines = [] 696 for name, value in report_attrs: 697 line = self.HEADING_ATTRIBUTE_TMPL % dict( 698 name = saxutils.escape(name), 699 value = saxutils.escape(value), 700 ) 701 a_lines.append(line) 702 heading = self.HEADING_TMPL % dict( 703 title = saxutils.escape(self.title), 704 parameters = ''.join(a_lines), 705 description = saxutils.escape(self.description), 706 ) 707 return heading 708 709 710 def _generate_report(self, result): 711 rows = [] 712 sortedResult = self.sortResult(result.result) 713 for cid, (cls, cls_results) in enumerate(sortedResult): 714 # subtotal for a class 715 np = nf = ne = 0 716 for n,t,o,e in cls_results: 717 if n == 0: np += 1 718 elif n == 1: nf += 1 719 else: ne += 1 720 721 # format class description 722 if cls.__module__ == "__main__": 723 name = cls.__name__ 724 else: 725 name = "%s.%s" % (cls.__module__, cls.__name__) 726 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 727 desc = doc and '%s: %s' % (name, doc) or name 728 729 row = self.REPORT_CLASS_TMPL % dict( 730 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 731 desc = desc, 732 count = np+nf+ne, 733 Pass = np, 734 fail = nf, 735 error = ne, 736 cid = 'c%s' % (cid+1), 737 ) 738 rows.append(row) 739 740 for tid, (n,t,o,e) in enumerate(cls_results): 741 self._generate_report_test(rows, cid, tid, n, t, o, e) 742 743 report = self.REPORT_TMPL % dict( 744 test_list = ''.join(rows), 745 count = str(result.success_count+result.failure_count+result.error_count), 746 Pass = str(result.success_count), 747 fail = str(result.failure_count), 748 error = str(result.error_count), 749 ) 750 return report 751 752 753 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 754 # e.g. 'pt1.1', 'ft1.1', etc 755 has_output = bool(o or e) 756 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 757 name = t.id().split('.')[-1] 758 doc = t.shortDescription() or "" 759 desc = doc and ('%s: %s' % (name, doc)) or name 760 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 761 762 # o and e should be byte string because they are collected from stdout and stderr? 763 if isinstance(o,str): 764 # TODO: some problem with 'string_escape': it escape and mess up formating 765 # uo = unicode(o.encode('string_escape')) 766 uo = o.decode('latin-1') 767 else: 768 uo = o 769 if isinstance(e,str): 770 # TODO: some problem with 'string_escape': it escape and mess up formating 771 # ue = unicode(e.encode('string_escape')) 772 ue = e 773 else: 774 ue = e 775 776 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 777 id = tid, 778 output = saxutils.escape(str(uo)+ue), 779 ) 780 781 row = tmpl % dict( 782 tid = tid, 783 Class = (n == 0 and 'hiddenRow' or 'none'), 784 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 785 desc = desc, 786 script = script, 787 status = self.STATUS[n], 788 ) 789 rows.append(row) 790 if not has_output: 791 return 792 793 def _generate_ending(self): 794 return self.ENDING_TMPL 795 796 797 ############################################################################## 798 # Facilities for running tests from the command line 799 ############################################################################## 800 801 # Note: Reuse unittest.TestProgram to launch test. In the future we may 802 # build our own launcher to support more specific command line 803 # parameters like test title, CSS, etc. 804 class TestProgram(unittest.TestProgram): 805 """ 806 A variation of the unittest.TestProgram. Please refer to the base 807 class for command line parameters. 808 """ 809 def runTests(self): 810 # Pick HTMLTestRunner as the default test runner. 811 # base class's testRunner parameter is not useful because it means 812 # we have to instantiate HTMLTestRunner before we know self.verbosity. 813 if self.testRunner is None: 814 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 815 unittest.TestProgram.runTests(self) 816 817 main = TestProgram 818 819 ############################################################################## 820 # Executing this module from the command line 821 ############################################################################## 822 823 if __name__ == "__main__": 824 main(module=None)
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 = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 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 <script> block as CDATA. 89 90 Version in 0.7.1 91 * Back port to Python 2.3 (Frank Horowitz). 92 * Fix missing scroll bars in detail log (Podi). 93 """ 94 95 # TODO: color stderr 96 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 97 98 import datetime 99 # import StringIO 100 import io 101 import sys 102 import time 103 import unittest 104 from xml.sax import saxutils 105 106 107 # ------------------------------------------------------------------------ 108 # The redirectors below are used to capture output during testing. Output 109 # sent to sys.stdout and sys.stderr are automatically captured. However 110 # in some cases sys.stdout is already cached before BSTestRunner is 111 # invoked (e.g. calling logging.basicConfig). In order to capture those 112 # output, use the redirectors for the cached stream. 113 # 114 # e.g. 115 # >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector) 116 # >>> 117 118 def to_unicode(s): 119 try: 120 return unicode(s) 121 except UnicodeDecodeError: 122 # s is non ascii byte string 123 return s.decode('unicode_escape') 124 125 class OutputRedirector(object): 126 """ Wrapper to redirect stdout or stderr """ 127 def __init__(self, fp): 128 self.fp = fp 129 130 def write(self, s): 131 self.fp.write(to_unicode(s)) 132 133 def writelines(self, lines): 134 lines = map(to_unicode, lines) 135 self.fp.writelines(lines) 136 137 def flush(self): 138 self.fp.flush() 139 140 stdout_redirector = OutputRedirector(sys.stdout) 141 stderr_redirector = OutputRedirector(sys.stderr) 142 143 144 145 # ---------------------------------------------------------------------- 146 # Template 147 148 class Template_mixin(object): 149 """ 150 Define a HTML template for report customerization and generation. 151 152 Overall structure of an HTML report 153 154 HTML 155 +------------------------+ 156 |<html> | 157 | <head> | 158 | | 159 | STYLESHEET | 160 | +----------------+ | 161 | | | | 162 | +----------------+ | 163 | | 164 | </head> | 165 | | 166 | <body> | 167 | | 168 | HEADING | 169 | +----------------+ | 170 | | | | 171 | +----------------+ | 172 | | 173 | REPORT | 174 | +----------------+ | 175 | | | | 176 | +----------------+ | 177 | | 178 | ENDING | 179 | +----------------+ | 180 | | | | 181 | +----------------+ | 182 | | 183 | </body> | 184 |</html> | 185 +------------------------+ 186 """ 187 188 STATUS = { 189 0: 'pass', 190 1: 'fail', 191 2: 'error', 192 } 193 194 DEFAULT_TITLE = 'Unit Test Report' 195 DEFAULT_DESCRIPTION = '' 196 197 # ------------------------------------------------------------------------ 198 # HTML Template 199 200 HTML_TMPL = r"""<!DOCTYPE html> 201 <html lang="zh-cn"> 202 <head> 203 <meta charset="utf-8"> 204 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 205 <meta name="viewport" content="width=device-width, initial-scale=1"> 206 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 207 <title>%(title)s</title> 208 <meta name="generator" content="%(generator)s"/> 209 <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css"> 210 %(stylesheet)s 211 212 <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> 213 <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> 214 <!--[if lt IE 9]> 215 <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script> 216 <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script> 217 <![endif]--> 218 </head> 219 <body> 220 <script language="javascript" type="text/javascript"><!-- 221 output_list = Array(); 222 223 /* level - 0:Summary; 1:Failed; 2:All */ 224 function showCase(level) { 225 trs = document.getElementsByTagName("tr"); 226 for (var i = 0; i < trs.length; i++) { 227 tr = trs[i]; 228 id = tr.id; 229 if (id.substr(0,2) == 'ft') { 230 if (level < 1) { 231 tr.className = 'hiddenRow'; 232 } 233 else { 234 tr.className = ''; 235 } 236 } 237 if (id.substr(0,2) == 'pt') { 238 if (level > 1) { 239 tr.className = ''; 240 } 241 else { 242 tr.className = 'hiddenRow'; 243 } 244 } 245 } 246 } 247 248 249 function showClassDetail(cid, count) { 250 var id_list = Array(count); 251 var toHide = 1; 252 for (var i = 0; i < count; i++) { 253 tid0 = 't' + cid.substr(1) + '.' + (i+1); 254 tid = 'f' + tid0; 255 tr = document.getElementById(tid); 256 if (!tr) { 257 tid = 'p' + tid0; 258 tr = document.getElementById(tid); 259 } 260 id_list[i] = tid; 261 if (tr.className) { 262 toHide = 0; 263 } 264 } 265 for (var i = 0; i < count; i++) { 266 tid = id_list[i]; 267 if (toHide) { 268 document.getElementById('div_'+tid).style.display = 'none' 269 document.getElementById(tid).className = 'hiddenRow'; 270 } 271 else { 272 document.getElementById(tid).className = ''; 273 } 274 } 275 } 276 277 278 function showTestDetail(div_id){ 279 var details_div = document.getElementById(div_id) 280 var displayState = details_div.style.display 281 // alert(displayState) 282 if (displayState != 'block' ) { 283 displayState = 'block' 284 details_div.style.display = 'block' 285 } 286 else { 287 details_div.style.display = 'none' 288 } 289 } 290 291 292 function html_escape(s) { 293 s = s.replace(/&/g,'&'); 294 s = s.replace(/</g,'<'); 295 s = s.replace(/>/g,'>'); 296 return s; 297 } 298 299 /* obsoleted by detail in <div> 300 function showOutput(id, name) { 301 var w = window.open("", //url 302 name, 303 "resizable,scrollbars,status,width=800,height=450"); 304 d = w.document; 305 d.write("<pre>"); 306 d.write(html_escape(output_list[id])); 307 d.write(" "); 308 d.write("<a href='javascript:window.close()'>close</a> "); 309 d.write("</pre> "); 310 d.close(); 311 } 312 */ 313 --></script> 314 315 <div class="container"> 316 %(heading)s 317 %(report)s 318 %(ending)s 319 </div> 320 321 </body> 322 </html> 323 """ 324 # variables: (title, generator, stylesheet, heading, report, ending) 325 326 327 # ------------------------------------------------------------------------ 328 # Stylesheet 329 # 330 # alternatively use a <link> for external style sheet, e.g. 331 # <link rel="stylesheet" href="$url" type="text/css"> 332 333 STYLESHEET_TMPL = """ 334 <style type="text/css" media="screen"> 335 336 /* -- css div popup ------------------------------------------------------------------------ */ 337 .popup_window { 338 display: none; 339 position: relative; 340 left: 0px; 341 top: 0px; 342 /*border: solid #627173 1px; */ 343 padding: 10px; 344 background-color: #99CCFF; 345 font-family: "Lucida Console", "Courier New", Courier, monospace; 346 text-align: left; 347 font-size: 10pt; 348 500px; 349 } 350 351 /* -- report ------------------------------------------------------------------------ */ 352 353 #show_detail_line .label { 354 font-size: 85%; 355 cursor: pointer; 356 } 357 358 #show_detail_line { 359 margin: 2em auto 1em auto; 360 } 361 362 #total_row { font-weight: bold; } 363 .hiddenRow { display: none; } 364 .testcase { margin-left: 2em; } 365 366 </style> 367 """ 368 369 370 371 # ------------------------------------------------------------------------ 372 # Heading 373 # 374 375 HEADING_TMPL = """<div class='heading'> 376 <h1>%(title)s</h1> 377 %(parameters)s 378 <p class='description'>%(description)s</p> 379 </div> 380 381 """ # variables: (title, parameters, description) 382 383 HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p> 384 """ # variables: (name, value) 385 386 387 388 # ------------------------------------------------------------------------ 389 # Report 390 # 391 392 REPORT_TMPL = """ 393 <p id='show_detail_line'> 394 <span class="label label-primary" onclick="showCase(0)">Summary</span> 395 <span class="label label-danger" onclick="showCase(1)">Failed</span> 396 <span class="label label-default" onclick="showCase(2)">All</span> 397 </p> 398 <table id='result_table' class="table"> 399 <thead> 400 <tr id='header_row'> 401 <th>Test Group/Test case</td> 402 <th>Count</td> 403 <th>Pass</td> 404 <th>Fail</td> 405 <th>Error</td> 406 <th>View</td> 407 </tr> 408 </thead> 409 <tbody> 410 %(test_list)s 411 </tbody> 412 <tfoot> 413 <tr id='total_row'> 414 <td>Total</td> 415 <td>%(count)s</td> 416 <td class="text text-success">%(Pass)s</td> 417 <td class="text text-danger">%(fail)s</td> 418 <td class="text text-warning">%(error)s</td> 419 <td> </td> 420 </tr> 421 </tfoot> 422 </table> 423 """ # variables: (test_list, count, Pass, fail, error) 424 425 REPORT_CLASS_TMPL = r""" 426 <tr class='%(style)s'> 427 <td>%(desc)s</td> 428 <td>%(count)s</td> 429 <td>%(Pass)s</td> 430 <td>%(fail)s</td> 431 <td>%(error)s</td> 432 <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 433 </tr> 434 """ # variables: (style, desc, count, Pass, fail, error, cid) 435 436 437 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 438 <tr id='%(tid)s' class='%(Class)s'> 439 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 440 <td colspan='5' align='center'> 441 442 <!--css div popup start--> 443 <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 444 %(status)s</a> 445 446 <div id='div_%(tid)s' class="popup_window"> 447 <div style='text-align: right;cursor:pointer'> 448 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 449 [x]</a> 450 </div> 451 <pre> 452 %(script)s 453 </pre> 454 </div> 455 <!--css div popup end--> 456 457 </td> 458 </tr> 459 """ # variables: (tid, Class, style, desc, status) 460 461 462 REPORT_TEST_NO_OUTPUT_TMPL = r""" 463 <tr id='%(tid)s' class='%(Class)s'> 464 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 465 <td colspan='5' align='center'>%(status)s</td> 466 </tr> 467 """ # variables: (tid, Class, style, desc, status) 468 469 470 REPORT_TEST_OUTPUT_TMPL = r""" 471 %(id)s: %(output)s 472 """ # variables: (id, output) 473 474 475 476 # ------------------------------------------------------------------------ 477 # ENDING 478 # 479 480 ENDING_TMPL = """<div id='ending'> </div>""" 481 482 # -------------------- The end of the Template class ------------------- 483 484 485 TestResult = unittest.TestResult 486 487 class _TestResult(TestResult): 488 # note: _TestResult is a pure representation of results. 489 # It lacks the output and reporting ability compares to unittest._TextTestResult. 490 491 def __init__(self, verbosity=1): 492 TestResult.__init__(self) 493 # self.outputBuffer = StringIO.StringIO() 494 self.outputBuffer = io.StringIO() 495 self.stdout0 = None 496 self.stderr0 = None 497 self.success_count = 0 498 self.failure_count = 0 499 self.error_count = 0 500 self.verbosity = verbosity 501 502 # result is a list of result in 4 tuple 503 # ( 504 # result code (0: success; 1: fail; 2: error), 505 # TestCase object, 506 # Test output (byte string), 507 # stack trace, 508 # ) 509 self.result = [] 510 511 512 def startTest(self, test): 513 TestResult.startTest(self, test) 514 # just one buffer for both stdout and stderr 515 stdout_redirector.fp = self.outputBuffer 516 stderr_redirector.fp = self.outputBuffer 517 self.stdout0 = sys.stdout 518 self.stderr0 = sys.stderr 519 sys.stdout = stdout_redirector 520 sys.stderr = stderr_redirector 521 522 523 def complete_output(self): 524 """ 525 Disconnect output redirection and return buffer. 526 Safe to call multiple times. 527 """ 528 if self.stdout0: 529 sys.stdout = self.stdout0 530 sys.stderr = self.stderr0 531 self.stdout0 = None 532 self.stderr0 = None 533 return self.outputBuffer.getvalue() 534 535 536 def stopTest(self, test): 537 # Usually one of addSuccess, addError or addFailure would have been called. 538 # But there are some path in unittest that would bypass this. 539 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 540 self.complete_output() 541 542 543 def addSuccess(self, test): 544 self.success_count += 1 545 TestResult.addSuccess(self, test) 546 output = self.complete_output() 547 self.result.append((0, test, output, '')) 548 if self.verbosity > 1: 549 sys.stderr.write('ok ') 550 sys.stderr.write(str(test)) 551 sys.stderr.write(' ') 552 else: 553 sys.stderr.write('.') 554 555 def addError(self, test, err): 556 self.error_count += 1 557 TestResult.addError(self, test, err) 558 _, _exc_str = self.errors[-1] 559 output = self.complete_output() 560 self.result.append((2, test, output, _exc_str)) 561 if self.verbosity > 1: 562 sys.stderr.write('E ') 563 sys.stderr.write(str(test)) 564 sys.stderr.write(' ') 565 else: 566 sys.stderr.write('E') 567 568 def addFailure(self, test, err): 569 self.failure_count += 1 570 TestResult.addFailure(self, test, err) 571 _, _exc_str = self.failures[-1] 572 output = self.complete_output() 573 self.result.append((1, test, output, _exc_str)) 574 if self.verbosity > 1: 575 sys.stderr.write('F ') 576 sys.stderr.write(str(test)) 577 sys.stderr.write(' ') 578 else: 579 sys.stderr.write('F') 580 581 582 class BSTestRunner(Template_mixin): 583 """ 584 """ 585 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 586 self.stream = stream 587 self.verbosity = verbosity 588 if title is None: 589 self.title = self.DEFAULT_TITLE 590 else: 591 self.title = title 592 if description is None: 593 self.description = self.DEFAULT_DESCRIPTION 594 else: 595 self.description = description 596 597 self.startTime = datetime.datetime.now() 598 599 600 def run(self, test): 601 "Run the given test case or test suite." 602 result = _TestResult(self.verbosity) 603 test(result) 604 self.stopTime = datetime.datetime.now() 605 self.generateReport(test, result) 606 # print >>sys.stderr, ' Time Elapsed: %s' % (self.stopTime-self.startTime) 607 print(sys.stderr, ' Time Elapsed: %s' % (self.stopTime - self.startTime)) 608 return result 609 610 611 def sortResult(self, result_list): 612 # unittest does not seems to run in any particular order. 613 # Here at least we want to group them together by class. 614 rmap = {} 615 classes = [] 616 for n,t,o,e in result_list: 617 cls = t.__class__ 618 # if not rmap.has_key(cls): 619 if not cls in rmap: 620 rmap[cls] = [] 621 classes.append(cls) 622 rmap[cls].append((n,t,o,e)) 623 r = [(cls, rmap[cls]) for cls in classes] 624 return r 625 626 627 def getReportAttributes(self, result): 628 """ 629 Return report attributes as a list of (name, value). 630 Override this to add custom attributes. 631 """ 632 startTime = str(self.startTime)[:19] 633 duration = str(self.stopTime - self.startTime) 634 status = [] 635 if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>' % result.success_count) 636 if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count) 637 if result.error_count: status.append('<span class="text text-warning">Error <strong>%s</strong></span>' % result.error_count ) 638 if status: 639 status = ' '.join(status) 640 else: 641 status = 'none' 642 return [ 643 ('Start Time', startTime), 644 ('Duration', duration), 645 ('Status', status), 646 ] 647 648 649 def generateReport(self, test, result): 650 report_attrs = self.getReportAttributes(result) 651 generator = 'BSTestRunner %s' % __version__ 652 stylesheet = self._generate_stylesheet() 653 heading = self._generate_heading(report_attrs) 654 report = self._generate_report(result) 655 ending = self._generate_ending() 656 output = self.HTML_TMPL % dict( 657 title = saxutils.escape(self.title), 658 generator = generator, 659 stylesheet = stylesheet, 660 heading = heading, 661 report = report, 662 ending = ending, 663 ) 664 self.stream.write(output.encode('utf8')) 665 666 667 def _generate_stylesheet(self): 668 return self.STYLESHEET_TMPL 669 670 671 def _generate_heading(self, report_attrs): 672 a_lines = [] 673 for name, value in report_attrs: 674 line = self.HEADING_ATTRIBUTE_TMPL % dict( 675 # name = saxutils.escape(name), 676 # value = saxutils.escape(value), 677 name = name, 678 value = value, 679 ) 680 a_lines.append(line) 681 heading = self.HEADING_TMPL % dict( 682 title = saxutils.escape(self.title), 683 parameters = ''.join(a_lines), 684 description = saxutils.escape(self.description), 685 ) 686 return heading 687 688 689 def _generate_report(self, result): 690 rows = [] 691 sortedResult = self.sortResult(result.result) 692 for cid, (cls, cls_results) in enumerate(sortedResult): 693 # subtotal for a class 694 np = nf = ne = 0 695 for n,t,o,e in cls_results: 696 if n == 0: np += 1 697 elif n == 1: nf += 1 698 else: ne += 1 699 700 # format class description 701 if cls.__module__ == "__main__": 702 name = cls.__name__ 703 else: 704 name = "%s.%s" % (cls.__module__, cls.__name__) 705 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 706 desc = doc and '%s: %s' % (name, doc) or name 707 708 row = self.REPORT_CLASS_TMPL % dict( 709 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success', 710 desc = desc, 711 count = np+nf+ne, 712 Pass = np, 713 fail = nf, 714 error = ne, 715 cid = 'c%s' % (cid+1), 716 ) 717 rows.append(row) 718 719 for tid, (n,t,o,e) in enumerate(cls_results): 720 self._generate_report_test(rows, cid, tid, n, t, o, e) 721 722 report = self.REPORT_TMPL % dict( 723 test_list = ''.join(rows), 724 count = str(result.success_count+result.failure_count+result.error_count), 725 Pass = str(result.success_count), 726 fail = str(result.failure_count), 727 error = str(result.error_count), 728 ) 729 return report 730 731 732 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 733 # e.g. 'pt1.1', 'ft1.1', etc 734 has_output = bool(o or e) 735 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 736 name = t.id().split('.')[-1] 737 doc = t.shortDescription() or "" 738 desc = doc and ('%s: %s' % (name, doc)) or name 739 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 740 741 # o and e should be byte string because they are collected from stdout and stderr? 742 if isinstance(o,str): 743 # TODO: some problem with 'string_escape': it escape and mess up formating 744 # uo = unicode(o.encode('string_escape')) 745 # uo = o.decode('latin-1') 746 uo = o 747 else: 748 uo = o 749 if isinstance(e,str): 750 # TODO: some problem with 'string_escape': it escape and mess up formating 751 # ue = unicode(e.encode('string_escape')) 752 # ue = e.decode('latin-1') 753 ue=e 754 else: 755 ue = e 756 757 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 758 id = tid, 759 output = saxutils.escape(uo+ue), 760 ) 761 762 row = tmpl % dict( 763 tid = tid, 764 # Class = (n == 0 and 'hiddenRow' or 'none'), 765 Class = (n == 0 and 'hiddenRow' or 'text text-success'), 766 # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 767 style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'), 768 desc = desc, 769 script = script, 770 status = self.STATUS[n], 771 ) 772 rows.append(row) 773 if not has_output: 774 return 775 776 def _generate_ending(self): 777 return self.ENDING_TMPL 778 779 780 ############################################################################## 781 # Facilities for running tests from the command line 782 ############################################################################## 783 784 # Note: Reuse unittest.TestProgram to launch test. In the future we may 785 # build our own launcher to support more specific command line 786 # parameters like test title, CSS, etc. 787 class TestProgram(unittest.TestProgram): 788 """ 789 A variation of the unittest.TestProgram. Please refer to the base 790 class for command line parameters. 791 """ 792 def runTests(self): 793 # Pick BSTestRunner as the default test runner. 794 # base class's testRunner parameter is not useful because it means 795 # we have to instantiate BSTestRunner before we know self.verbosity. 796 if self.testRunner is None: 797 self.testRunner = BSTestRunner(verbosity=self.verbosity) 798 unittest.TestProgram.runTests(self) 799 800 main = TestProgram 801 802 ############################################################################## 803 # Executing this module from the command line 804 ############################################################################## 805 806 if __name__ == "__main__": 807 main(module=None)
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 StringIO 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 500px; 353 } 354 355 } 356 /* -- report ------------------------------------------------------------------------ */ 357 #show_detail_line { 358 margin-top: 3ex; 359 margin-bottom: 1ex; 360 } 361 #result_table { 362 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 = StringIO.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 607 class HTMLTestRunner(Template_mixin): 608 """ 609 """ 610 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 611 self.stream = stream 612 self.verbosity = verbosity 613 if title is None: 614 self.title = self.DEFAULT_TITLE 615 else: 616 self.title = title 617 if description is None: 618 self.description = self.DEFAULT_DESCRIPTION 619 else: 620 self.description = description 621 622 self.startTime = datetime.datetime.now() 623 624 625 def run(self, test): 626 "Run the given test case or test suite." 627 result = _TestResult(self.verbosity) 628 test(result) 629 self.stopTime = datetime.datetime.now() 630 self.generateReport(test, result) 631 print >>sys.stderr, ' Time Elapsed: %s' % (self.stopTime-self.startTime) 632 return result 633 634 635 def sortResult(self, result_list): 636 # unittest does not seems to run in any particular order. 637 # Here at least we want to group them together by class. 638 rmap = {} 639 classes = [] 640 for n,t,o,e in result_list: 641 cls = t.__class__ 642 if not rmap.has_key(cls): 643 rmap[cls] = [] 644 classes.append(cls) 645 rmap[cls].append((n,t,o,e)) 646 r = [(cls, rmap[cls]) for cls in classes] 647 return r 648 649 650 def getReportAttributes(self, result): 651 """ 652 Return report attributes as a list of (name, value). 653 Override this to add custom attributes. 654 """ 655 startTime = str(self.startTime)[:19] 656 duration = str(self.stopTime - self.startTime) 657 status = [] 658 if result.success_count: status.append('Pass %s' % result.success_count) 659 if result.failure_count: status.append('Failure %s' % result.failure_count) 660 if result.error_count: status.append('Error %s' % result.error_count ) 661 if status: 662 status = ' '.join(status) 663 else: 664 status = 'none' 665 return [ 666 ('Start Time', startTime), 667 ('Duration', duration), 668 ('Status', status), 669 ] 670 671 672 def generateReport(self, test, result): 673 report_attrs = self.getReportAttributes(result) 674 generator = 'HTMLTestRunner %s' % __version__ 675 stylesheet = self._generate_stylesheet() 676 heading = self._generate_heading(report_attrs) 677 report = self._generate_report(result) 678 ending = self._generate_ending() 679 output = self.HTML_TMPL % dict( 680 title = saxutils.escape(self.title), 681 generator = generator, 682 stylesheet = stylesheet, 683 heading = heading, 684 report = report, 685 ending = ending, 686 ) 687 self.stream.write(output.encode('utf8')) 688 689 690 def _generate_stylesheet(self): 691 return self.STYLESHEET_TMPL 692 693 694 def _generate_heading(self, report_attrs): 695 a_lines = [] 696 for name, value in report_attrs: 697 line = self.HEADING_ATTRIBUTE_TMPL % dict( 698 name = saxutils.escape(name), 699 value = saxutils.escape(value), 700 ) 701 a_lines.append(line) 702 heading = self.HEADING_TMPL % dict( 703 title = saxutils.escape(self.title), 704 parameters = ''.join(a_lines), 705 description = saxutils.escape(self.description), 706 ) 707 return heading 708 709 710 def _generate_report(self, result): 711 rows = [] 712 sortedResult = self.sortResult(result.result) 713 for cid, (cls, cls_results) in enumerate(sortedResult): 714 # subtotal for a class 715 np = nf = ne = 0 716 for n,t,o,e in cls_results: 717 if n == 0: np += 1 718 elif n == 1: nf += 1 719 else: ne += 1 720 721 # format class description 722 if cls.__module__ == "__main__": 723 name = cls.__name__ 724 else: 725 name = "%s.%s" % (cls.__module__, cls.__name__) 726 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 727 desc = doc and '%s: %s' % (name, doc) or name 728 729 row = self.REPORT_CLASS_TMPL % dict( 730 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 731 desc = desc, 732 count = np+nf+ne, 733 Pass = np, 734 fail = nf, 735 error = ne, 736 cid = 'c%s' % (cid+1), 737 ) 738 rows.append(row) 739 740 for tid, (n,t,o,e) in enumerate(cls_results): 741 self._generate_report_test(rows, cid, tid, n, t, o, e) 742 743 report = self.REPORT_TMPL % dict( 744 test_list = ''.join(rows), 745 count = str(result.success_count+result.failure_count+result.error_count), 746 Pass = str(result.success_count), 747 fail = str(result.failure_count), 748 error = str(result.error_count), 749 ) 750 return report 751 752 753 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 754 # e.g. 'pt1.1', 'ft1.1', etc 755 has_output = bool(o or e) 756 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 757 name = t.id().split('.')[-1] 758 doc = t.shortDescription() or "" 759 desc = doc and ('%s: %s' % (name, doc)) or name 760 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 761 762 # o and e should be byte string because they are collected from stdout and stderr? 763 if isinstance(o,str): 764 # TODO: some problem with 'string_escape': it escape and mess up formating 765 # uo = unicode(o.encode('string_escape')) 766 uo = o.decode('latin-1') 767 else: 768 uo = o 769 if isinstance(e,str): 770 # TODO: some problem with 'string_escape': it escape and mess up formating 771 # ue = unicode(e.encode('string_escape')) 772 ue = e.decode('latin-1') 773 else: 774 ue = e 775 776 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 777 id = tid, 778 output = saxutils.escape(uo+ue), 779 ) 780 781 row = tmpl % dict( 782 tid = tid, 783 Class = (n == 0 and 'hiddenRow' or 'none'), 784 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 785 desc = desc, 786 script = script, 787 status = self.STATUS[n], 788 ) 789 rows.append(row) 790 if not has_output: 791 return 792 793 def _generate_ending(self): 794 return self.ENDING_TMPL 795 796 797 ############################################################################## 798 # Facilities for running tests from the command line 799 ############################################################################## 800 801 # Note: Reuse unittest.TestProgram to launch test. In the future we may 802 # build our own launcher to support more specific command line 803 # parameters like test title, CSS, etc. 804 class TestProgram(unittest.TestProgram): 805 """ 806 A variation of the unittest.TestProgram. Please refer to the base 807 class for command line parameters. 808 """ 809 def runTests(self): 810 # Pick HTMLTestRunner as the default test runner. 811 # base class's testRunner parameter is not useful because it means 812 # we have to instantiate HTMLTestRunner before we know self.verbosity. 813 if self.testRunner is None: 814 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 815 unittest.TestProgram.runTests(self) 816 817 main = TestProgram 818 819 ############################################################################## 820 # Executing this module from the command line 821 ############################################################################## 822 823 if __name__ == "__main__": 824 main(module=None)
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 The simplest way to use this is to invoke its main method. E.g. 4 import unittest 5 import BSTestRunner 6 ... define your tests ... 7 if __name__ == '__main__': 8 BSTestRunner.main() 9 For more customization options, instantiates a BSTestRunner object. 10 BSTestRunner is a counterpart to unittest's TextTestRunner. E.g. 11 # output to a file 12 fp = file('my_report.html', 'wb') 13 runner = BSTestRunner.BSTestRunner( 14 stream=fp, 15 title='My unit test', 16 description='This demonstrates the report output by BSTestRunner.' 17 ) 18 # Use an external stylesheet. 19 # See the Template_mixin class for more customizable options 20 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 21 # run the test 22 runner.run(my_test_suite) 23 ------------------------------------------------------------------------ 24 Copyright (c) 2004-2007, Wai Yip Tung 25 Copyright (c) 2016, Eason Han 26 All rights reserved. 27 Redistribution and use in source and binary forms, with or without 28 modification, are permitted provided that the following conditions are 29 met: 30 * Redistributions of source code must retain the above copyright notice, 31 this list of conditions and the following disclaimer. 32 * Redistributions in binary form must reproduce the above copyright 33 notice, this list of conditions and the following disclaimer in the 34 documentation and/or other materials provided with the distribution. 35 * Neither the name Wai Yip Tung nor the names of its contributors may be 36 used to endorse or promote products derived from this software without 37 specific prior written permission. 38 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 39 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 40 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 41 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 42 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 43 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 44 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 45 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 46 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 47 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 48 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 49 """ 50 51 52 __author__ = "Wai Yip Tung && Eason Han" 53 __version__ = "0.8.4" 54 55 56 """ 57 Change History 58 Version 0.8.3 59 * Modify html style using bootstrap3. 60 Version 0.8.3 61 * Prevent crash on class or module-level exceptions (Darren Wurf). 62 Version 0.8.2 63 * Show output inline instead of popup window (Viorel Lupu). 64 Version in 0.8.1 65 * Validated XHTML (Wolfgang Borgert). 66 * Added description of test classes and test cases. 67 Version in 0.8.0 68 * Define Template_mixin class for customization. 69 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 70 Version in 0.7.1 71 * Back port to Python 2.3 (Frank Horowitz). 72 * Fix missing scroll bars in detail log (Podi). 73 """ 74 75 # TODO: color stderr 76 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 77 78 import datetime 79 try: 80 from StringIO import StringIO 81 except ImportError: 82 from io import StringIO 83 import sys 84 import time 85 import unittest 86 from xml.sax import saxutils 87 88 89 # ------------------------------------------------------------------------ 90 # The redirectors below are used to capture output during testing. Output 91 # sent to sys.stdout and sys.stderr are automatically captured. However 92 # in some cases sys.stdout is already cached before BSTestRunner is 93 # invoked (e.g. calling logging.basicConfig). In order to capture those 94 # output, use the redirectors for the cached stream. 95 # 96 # e.g. 97 # >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector) 98 # >>> 99 100 def to_unicode(s): 101 try: 102 return unicode(s) 103 except UnicodeDecodeError: 104 # s is non ascii byte string 105 return s.decode('unicode_escape') 106 107 class OutputRedirector(object): 108 """ Wrapper to redirect stdout or stderr """ 109 def __init__(self, fp): 110 self.fp = fp 111 112 def write(self, s): 113 self.fp.write(to_unicode(s)) 114 115 def writelines(self, lines): 116 lines = map(to_unicode, lines) 117 self.fp.writelines(lines) 118 119 def flush(self): 120 self.fp.flush() 121 122 stdout_redirector = OutputRedirector(sys.stdout) 123 stderr_redirector = OutputRedirector(sys.stderr) 124 125 126 127 # ---------------------------------------------------------------------- 128 # Template 129 130 class Template_mixin(object): 131 """ 132 Define a HTML template for report customerization and generation. 133 Overall structure of an HTML report 134 HTML 135 +------------------------+ 136 |<html> | 137 | <head> | 138 | | 139 | STYLESHEET | 140 | +----------------+ | 141 | | | | 142 | +----------------+ | 143 | | 144 | </head> | 145 | | 146 | <body> | 147 | | 148 | HEADING | 149 | +----------------+ | 150 | | | | 151 | +----------------+ | 152 | | 153 | REPORT | 154 | +----------------+ | 155 | | | | 156 | +----------------+ | 157 | | 158 | ENDING | 159 | +----------------+ | 160 | | | | 161 | +----------------+ | 162 | | 163 | </body> | 164 |</html> | 165 +------------------------+ 166 """ 167 168 STATUS = { 169 0: 'pass', 170 1: 'fail', 171 2: 'error', 172 } 173 174 DEFAULT_TITLE = 'Unit Test Report' 175 DEFAULT_DESCRIPTION = '' 176 177 # ------------------------------------------------------------------------ 178 # HTML Template 179 180 HTML_TMPL = r"""<!DOCTYPE html> 181 <html lang="zh-cn"> 182 <head> 183 <meta charset="utf-8"> 184 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 185 <meta name="viewport" content="width=device-width, initial-scale=1"> 186 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 187 <title>%(title)s</title> 188 <meta name="generator" content="%(generator)s"/> 189 <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css"> 190 %(stylesheet)s 191 <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> 192 <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> 193 <!--[if lt IE 9]> 194 <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script> 195 <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script> 196 <![endif]--> 197 </head> 198 <body> 199 <script language="javascript" type="text/javascript"><!-- 200 output_list = Array(); 201 /* level - 0:Summary; 1:Failed; 2:All */ 202 function showCase(level) { 203 trs = document.getElementsByTagName("tr"); 204 for (var i = 0; i < trs.length; i++) { 205 tr = trs[i]; 206 id = tr.id; 207 if (id.substr(0,2) == 'ft') { 208 if (level < 1) { 209 tr.className = 'hiddenRow'; 210 } 211 else { 212 tr.className = ''; 213 } 214 } 215 if (id.substr(0,2) == 'pt') { 216 if (level > 1) { 217 tr.className = ''; 218 } 219 else { 220 tr.className = 'hiddenRow'; 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 function showTestDetail(div_id){ 253 var details_div = document.getElementById(div_id) 254 var displayState = details_div.style.display 255 // alert(displayState) 256 if (displayState != 'block' ) { 257 displayState = 'block' 258 details_div.style.display = 'block' 259 } 260 else { 261 details_div.style.display = 'none' 262 } 263 } 264 function html_escape(s) { 265 s = s.replace(/&/g,'&'); 266 s = s.replace(/</g,'<'); 267 s = s.replace(/>/g,'>'); 268 return s; 269 } 270 /* obsoleted by detail in <div> 271 function showOutput(id, name) { 272 var w = window.open("", //url 273 name, 274 "resizable,scrollbars,status,width=800,height=450"); 275 d = w.document; 276 d.write("<pre>"); 277 d.write(html_escape(output_list[id])); 278 d.write(" "); 279 d.write("<a href='javascript:window.close()'>close</a> "); 280 d.write("</pre> "); 281 d.close(); 282 } 283 */ 284 --></script> 285 <div class="container"> 286 %(heading)s 287 %(report)s 288 %(ending)s 289 </div> 290 </body> 291 </html> 292 """ 293 # variables: (title, generator, stylesheet, heading, report, ending) 294 295 296 # ------------------------------------------------------------------------ 297 # Stylesheet 298 # 299 # alternatively use a <link> for external style sheet, e.g. 300 # <link rel="stylesheet" href="$url" type="text/css"> 301 302 STYLESHEET_TMPL = """ 303 <style type="text/css" media="screen"> 304 /* -- css div popup ------------------------------------------------------------------------ */ 305 .popup_window { 306 display: none; 307 position: relative; 308 left: 0px; 309 top: 0px; 310 /*border: solid #627173 1px; */ 311 padding: 10px; 312 background-color: #99CCFF; 313 font-family: "Lucida Console", "Courier New", Courier, monospace; 314 text-align: left; 315 font-size: 10pt; 316 500px; 317 } 318 /* -- report ------------------------------------------------------------------------ */ 319 #show_detail_line .label { 320 font-size: 85%; 321 cursor: pointer; 322 } 323 #show_detail_line { 324 margin: 2em auto 1em auto; 325 } 326 #total_row { font-weight: bold; } 327 .hiddenRow { display: none; } 328 .testcase { margin-left: 2em; } 329 </style> 330 """ 331 332 333 334 # ------------------------------------------------------------------------ 335 # Heading 336 # 337 338 HEADING_TMPL = """<div class='heading'> 339 <h1>%(title)s</h1> 340 %(parameters)s 341 <p class='description'>%(description)s</p> 342 </div> 343 """ # variables: (title, parameters, description) 344 345 HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p> 346 """ # variables: (name, value) 347 348 349 350 # ------------------------------------------------------------------------ 351 # Report 352 # 353 354 REPORT_TMPL = """ 355 <p id='show_detail_line'> 356 <span class="label label-primary" onclick="showCase(0)">Summary</span> 357 <span class="label label-danger" onclick="showCase(1)">Failed</span> 358 <span class="label label-default" onclick="showCase(2)">All</span> 359 </p> 360 <table id='result_table' class="table"> 361 <thead> 362 <tr id='header_row'> 363 <th>Test Group/Test case</td> 364 <th>Count</td> 365 <th>Pass</td> 366 <th>Fail</td> 367 <th>Error</td> 368 <th>View</td> 369 </tr> 370 </thead> 371 <tbody> 372 %(test_list)s 373 </tbody> 374 <tfoot> 375 <tr id='total_row'> 376 <td>Total</td> 377 <td>%(count)s</td> 378 <td class="text text-success">%(Pass)s</td> 379 <td class="text text-danger">%(fail)s</td> 380 <td class="text text-warning">%(error)s</td> 381 <td> </td> 382 </tr> 383 </tfoot> 384 </table> 385 """ # variables: (test_list, count, Pass, fail, error) 386 387 REPORT_CLASS_TMPL = r""" 388 <tr class='%(style)s'> 389 <td>%(desc)s</td> 390 <td>%(count)s</td> 391 <td>%(Pass)s</td> 392 <td>%(fail)s</td> 393 <td>%(error)s</td> 394 <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 395 </tr> 396 """ # variables: (style, desc, count, Pass, fail, error, cid) 397 398 399 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 400 <tr id='%(tid)s' class='%(Class)s'> 401 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 402 <td colspan='5' align='center'> 403 <!--css div popup start--> 404 <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 405 %(status)s</a> 406 <div id='div_%(tid)s' class="popup_window"> 407 <div style='text-align: right;cursor:pointer'> 408 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 409 [x]</a> 410 </div> 411 <pre> 412 %(script)s 413 </pre> 414 </div> 415 <!--css div popup end--> 416 </td> 417 </tr> 418 """ # variables: (tid, Class, style, desc, status) 419 420 421 REPORT_TEST_NO_OUTPUT_TMPL = r""" 422 <tr id='%(tid)s' class='%(Class)s'> 423 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 424 <td colspan='5' align='center'>%(status)s</td> 425 </tr> 426 """ # variables: (tid, Class, style, desc, status) 427 428 429 REPORT_TEST_OUTPUT_TMPL = r""" 430 %(id)s: %(output)s 431 """ # variables: (id, output) 432 433 434 435 # ------------------------------------------------------------------------ 436 # ENDING 437 # 438 439 ENDING_TMPL = """<div id='ending'> </div>""" 440 441 # -------------------- The end of the Template class ------------------- 442 443 444 TestResult = unittest.TestResult 445 446 class _TestResult(TestResult): 447 # note: _TestResult is a pure representation of results. 448 # It lacks the output and reporting ability compares to unittest._TextTestResult. 449 450 def __init__(self, verbosity=1): 451 TestResult.__init__(self) 452 self.outputBuffer = StringIO() 453 self.stdout0 = None 454 self.stderr0 = None 455 self.success_count = 0 456 self.failure_count = 0 457 self.error_count = 0 458 self.verbosity = verbosity 459 460 # result is a list of result in 4 tuple 461 # ( 462 # result code (0: success; 1: fail; 2: error), 463 # TestCase object, 464 # Test output (byte string), 465 # stack trace, 466 # ) 467 self.result = [] 468 469 470 def startTest(self, test): 471 TestResult.startTest(self, test) 472 # just one buffer for both stdout and stderr 473 stdout_redirector.fp = self.outputBuffer 474 stderr_redirector.fp = self.outputBuffer 475 self.stdout0 = sys.stdout 476 self.stderr0 = sys.stderr 477 sys.stdout = stdout_redirector 478 sys.stderr = stderr_redirector 479 480 481 def complete_output(self): 482 """ 483 Disconnect output redirection and return buffer. 484 Safe to call multiple times. 485 """ 486 if self.stdout0: 487 sys.stdout = self.stdout0 488 sys.stderr = self.stderr0 489 self.stdout0 = None 490 self.stderr0 = None 491 return self.outputBuffer.getvalue() 492 493 494 def stopTest(self, test): 495 # Usually one of addSuccess, addError or addFailure would have been called. 496 # But there are some path in unittest that would bypass this. 497 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 498 self.complete_output() 499 500 501 def addSuccess(self, test): 502 self.success_count += 1 503 TestResult.addSuccess(self, test) 504 output = self.complete_output() 505 self.result.append((0, test, output, '')) 506 if self.verbosity > 1: 507 sys.stderr.write('ok ') 508 sys.stderr.write(str(test)) 509 sys.stderr.write(' ') 510 else: 511 sys.stderr.write('.') 512 513 def addError(self, test, err): 514 self.error_count += 1 515 TestResult.addError(self, test, err) 516 _, _exc_str = self.errors[-1] 517 output = self.complete_output() 518 self.result.append((2, test, output, _exc_str)) 519 if self.verbosity > 1: 520 sys.stderr.write('E ') 521 sys.stderr.write(str(test)) 522 sys.stderr.write(' ') 523 else: 524 sys.stderr.write('E') 525 526 def addFailure(self, test, err): 527 self.failure_count += 1 528 TestResult.addFailure(self, test, err) 529 _, _exc_str = self.failures[-1] 530 output = self.complete_output() 531 self.result.append((1, test, output, _exc_str)) 532 if self.verbosity > 1: 533 sys.stderr.write('F ') 534 sys.stderr.write(str(test)) 535 sys.stderr.write(' ') 536 else: 537 sys.stderr.write('F') 538 539 540 class BSTestRunner(Template_mixin): 541 """ 542 """ 543 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 544 self.stream = stream 545 self.verbosity = verbosity 546 if title is None: 547 self.title = self.DEFAULT_TITLE 548 else: 549 self.title = title 550 if description is None: 551 self.description = self.DEFAULT_DESCRIPTION 552 else: 553 self.description = description 554 555 self.startTime = datetime.datetime.now() 556 557 558 def run(self, test): 559 "Run the given test case or test suite." 560 result = _TestResult(self.verbosity) 561 test(result) 562 self.stopTime = datetime.datetime.now() 563 self.generateReport(test, result) 564 # print >>sys.stderr, ' Time Elapsed: %s' % (self.stopTime-self.startTime) 565 sys.stderr.write(' Time Elapsed: %s' % (self.stopTime-self.startTime)) 566 return result 567 568 569 def sortResult(self, result_list): 570 # unittest does not seems to run in any particular order. 571 # Here at least we want to group them together by class. 572 rmap = {} 573 classes = [] 574 for n,t,o,e in result_list: 575 cls = t.__class__ 576 # if not rmap.has_key(cls): 577 if not cls in rmap: 578 rmap[cls] = [] 579 classes.append(cls) 580 rmap[cls].append((n,t,o,e)) 581 r = [(cls, rmap[cls]) for cls in classes] 582 return r 583 584 585 def getReportAttributes(self, result): 586 """ 587 Return report attributes as a list of (name, value). 588 Override this to add custom attributes. 589 """ 590 startTime = str(self.startTime)[:19] 591 duration = str(self.stopTime - self.startTime) 592 status = [] 593 if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>' % result.success_count) 594 if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count) 595 if result.error_count: status.append('<span class="text text-warning">Error <strong>%s</strong></span>' % result.error_count ) 596 if status: 597 status = ' '.join(status) 598 else: 599 status = 'none' 600 return [ 601 ('Start Time', startTime), 602 ('Duration', duration), 603 ('Status', status), 604 ] 605 606 607 def generateReport(self, test, result): 608 report_attrs = self.getReportAttributes(result) 609 generator = 'BSTestRunner %s' % __version__ 610 stylesheet = self._generate_stylesheet() 611 heading = self._generate_heading(report_attrs) 612 report = self._generate_report(result) 613 ending = self._generate_ending() 614 output = self.HTML_TMPL % dict( 615 title = saxutils.escape(self.title), 616 generator = generator, 617 stylesheet = stylesheet, 618 heading = heading, 619 report = report, 620 ending = ending, 621 ) 622 try: 623 self.stream.write(output.encode('utf8')) 624 except: 625 self.stream.write(output) 626 627 628 def _generate_stylesheet(self): 629 return self.STYLESHEET_TMPL 630 631 632 def _generate_heading(self, report_attrs): 633 a_lines = [] 634 for name, value in report_attrs: 635 line = self.HEADING_ATTRIBUTE_TMPL % dict( 636 # name = saxutils.escape(name), 637 # value = saxutils.escape(value), 638 name = name, 639 value = value, 640 ) 641 a_lines.append(line) 642 heading = self.HEADING_TMPL % dict( 643 title = saxutils.escape(self.title), 644 parameters = ''.join(a_lines), 645 description = saxutils.escape(self.description), 646 ) 647 return heading 648 649 650 def _generate_report(self, result): 651 rows = [] 652 sortedResult = self.sortResult(result.result) 653 for cid, (cls, cls_results) in enumerate(sortedResult): 654 # subtotal for a class 655 np = nf = ne = 0 656 for n,t,o,e in cls_results: 657 if n == 0: np += 1 658 elif n == 1: nf += 1 659 else: ne += 1 660 661 # format class description 662 if cls.__module__ == "__main__": 663 name = cls.__name__ 664 else: 665 name = "%s.%s" % (cls.__module__, cls.__name__) 666 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 667 desc = doc and '%s: %s' % (name, doc) or name 668 669 row = self.REPORT_CLASS_TMPL % dict( 670 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success', 671 desc = desc, 672 count = np+nf+ne, 673 Pass = np, 674 fail = nf, 675 error = ne, 676 cid = 'c%s' % (cid+1), 677 ) 678 rows.append(row) 679 680 for tid, (n,t,o,e) in enumerate(cls_results): 681 self._generate_report_test(rows, cid, tid, n, t, o, e) 682 683 report = self.REPORT_TMPL % dict( 684 test_list = ''.join(rows), 685 count = str(result.success_count+result.failure_count+result.error_count), 686 Pass = str(result.success_count), 687 fail = str(result.failure_count), 688 error = str(result.error_count), 689 ) 690 return report 691 692 693 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 694 # e.g. 'pt1.1', 'ft1.1', etc 695 has_output = bool(o or e) 696 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 697 name = t.id().split('.')[-1] 698 doc = t.shortDescription() or "" 699 desc = doc and ('%s: %s' % (name, doc)) or name 700 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 701 702 # o and e should be byte string because they are collected from stdout and stderr? 703 if isinstance(o,str): 704 # TODO: some problem with 'string_escape': it escape and mess up formating 705 # uo = unicode(o.encode('string_escape')) 706 try: 707 uo = o.decode('latin-1') 708 except: 709 uo = o 710 else: 711 uo = o 712 if isinstance(e,str): 713 # TODO: some problem with 'string_escape': it escape and mess up formating 714 # ue = unicode(e.encode('string_escape')) 715 try: 716 ue = e.decode('latin-1') 717 except: 718 ue = e 719 else: 720 ue = e 721 722 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 723 id = tid, 724 output = saxutils.escape(uo+ue), 725 ) 726 727 row = tmpl % dict( 728 tid = tid, 729 # Class = (n == 0 and 'hiddenRow' or 'none'), 730 Class = (n == 0 and 'hiddenRow' or 'text text-success'), 731 # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 732 style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'), 733 desc = desc, 734 script = script, 735 status = self.STATUS[n], 736 ) 737 rows.append(row) 738 if not has_output: 739 return 740 741 def _generate_ending(self): 742 return self.ENDING_TMPL 743 744 745 ############################################################################## 746 # Facilities for running tests from the command line 747 ############################################################################## 748 749 # Note: Reuse unittest.TestProgram to launch test. In the future we may 750 # build our own launcher to support more specific command line 751 # parameters like test title, CSS, etc. 752 class TestProgram(unittest.TestProgram): 753 """ 754 A variation of the unittest.TestProgram. Please refer to the base 755 class for command line parameters. 756 """ 757 def runTests(self): 758 # Pick BSTestRunner as the default test runner. 759 # base class's testRunner parameter is not useful because it means 760 # we have to instantiate BSTestRunner before we know self.verbosity. 761 if self.testRunner is None: 762 self.testRunner = BSTestRunner(verbosity=self.verbosity) 763 unittest.TestProgram.runTests(self) 764 765 main = TestProgram 766 767 ############################################################################## 768 # Executing this module from the command line 769 ############################################################################## 770 771 if __name__ == "__main__": 772 main(module=None)
see also:
import
webbrowser
import
unittest
import
HTMLTestRunner
import
BSTestRunner
class
TestStringMethods(unittest.TestCase):
def
test_upper(
self
):
u
"""判断 foo.upper() 是否等于 FOO"""
self
.assertEqual(
'foo'
.upper(),
'FOO'
)
def
test_isupper(
self
):
u
""" 判断 Foo 是否为大写形式 """
self
.assertTrue(
'Foo'
.isupper())
if
__name__
=
=
'__main__'
:
suite
=
unittest.makeSuite(TestStringMethods)
f1
=
open
(
'result1.html'
,
'wb'
)
f2
=
open
(
'result2.html'
,
'wb'
)
HTMLTestRunner.HTMLTestRunner(
stream
=
f1,
title
=
u
'HTMLTestRunner版本关于upper的测试报告'
,
description
=
u
'判断upper的测试用例执行情况'
).run(suite)
suite
=
unittest.makeSuite(TestStringMethods)
BSTestRunner.BSTestRunner(
stream
=
f2,
title
=
u
'BSTestRunner版本关于upper的测试报告'
,
description
=
u
'判断upper的测试用例执行情况'
).run(suite)
f1.close()
f2.close()
webbrowser.
open
(
'result1.html'
)
webbrowser.
open
(
'result2.html'
)