Package unittests :: Module HTMLTestRunner
[hide private]
[frames] | no frames]

Source Code for Module unittests.HTMLTestRunner

  1  """ 
  2  A TestRunner for use with the Python unit testing framework. It 
  3  generates a HTML report to show the result at a glance. 
  4   
  5  The simplest way to use this is to invoke its main method. E.g. 
  6   
  7  import unittest 
  8  import HTMLTestRunner 
  9   
 10  ... define your tests ... 
 11   
 12  if __name__ == '__main__': 
 13  HTMLTestRunner.main() 
 14   
 15   
 16  For more customization options, instantiates a HTMLTestRunner object. 
 17  HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 
 18   
 19  # output to a file 
 20  fp = file('my_report.html', 'wb') 
 21  runner = HTMLTestRunner.HTMLTestRunner( 
 22  stream=fp, 
 23  title='My unit test', 
 24  description='This demonstrates the report output by HTMLTestRunner.' 
 25  ) 
 26   
 27  # Use an external stylesheet. 
 28  # See the Template_mixin class for more customizable options 
 29  runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 
 30   
 31  # run the test 
 32  runner.run(my_test_suite) 
 33   
 34   
 35  ------------------------------------------------------------------------ 
 36  Copyright (c) 2004-2007, Wai Yip Tung 
 37  All rights reserved. 
 38   
 39  Redistribution and use in source and binary forms, with or without 
 40  modification, are permitted provided that the following conditions are 
 41  met: 
 42   
 43  * Redistributions of source code must retain the above copyright notice, 
 44  this list of conditions and the following disclaimer. 
 45  * Redistributions in binary form must reproduce the above copyright 
 46  notice, this list of conditions and the following disclaimer in the 
 47  documentation and/or other materials provided with the distribution. 
 48  * Neither the name Wai Yip Tung nor the names of its contributors may be 
 49  used to endorse or promote products derived from this software without 
 50  specific prior written permission. 
 51   
 52  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 53  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 54  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
 55  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 
 56  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 57  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 58  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 59  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 60  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 61  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 62  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 63  """ 
 64   
 65  # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 
 66   
 67  __author__ = "Wai Yip Tung" 
 68  __version__ = "0.8.2" 
 69   
 70   
 71  """ 
 72  Change History 
 73   
 74  Version 0.8.2 
 75  * Show output inline instead of popup window (Viorel Lupu). 
 76   
 77  Version in 0.8.1 
 78  * Validated XHTML (Wolfgang Borgert). 
 79  * Added description of test classes and test cases. 
 80   
 81  Version in 0.8.0 
 82  * Define Template_mixin class for customization. 
 83  * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 
 84   
 85  Version in 0.7.1 
 86  * Back port to Python 2.3 (Frank Horowitz). 
 87  * Fix missing scroll bars in detail log (Podi). 
 88  """ 
 89   
 90  # TODO: color stderr 
 91  # TODO: simplify javascript using ,ore than 1 class in the class attribute? 
 92   
 93  import datetime 
 94  import 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,'&amp;'); 270 s = s.replace(/</g,'&lt;'); 271 s = s.replace(/>/g,'&gt;'); 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("\n"); 284 d.write("<a href='javascript:window.close()'>close</a>\n"); 285 d.write("</pre>\n"); 286 d.close(); 287 } 288 */ 289 --></script> 290 291 %(heading)s 292 %(report)s 293 %(ending)s 294 295 </body> 296 </html> 297 """ 298 # variables: (title, generator, stylesheet, heading, report, ending) 299 300 301 # ------------------------------------------------------------------------ 302 # Stylesheet 303 # 304 # alternatively use a <link> for external style sheet, e.g. 305 # <link rel="stylesheet" href="$url" type="text/css"> 306 307 STYLESHEET_TMPL = """ 308 <style type="text/css" media="screen"> 309 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } 310 table { font-size: 100%; } 311 pre { } 312 313 /* -- heading ---------------------------------------------------------------------- */ 314 h1 { 315 font-size: 16pt; 316 color: gray; 317 } 318 .heading { 319 margin-top: 0ex; 320 margin-bottom: 1ex; 321 } 322 323 .heading .attribute { 324 margin-top: 1ex; 325 margin-bottom: 0; 326 } 327 328 .heading .description { 329 margin-top: 4ex; 330 margin-bottom: 6ex; 331 } 332 333 /* -- css div popup ------------------------------------------------------------------------ */ 334 a.popup_link { 335 } 336 337 a.popup_link:hover { 338 color: red; 339 } 340 341 .popup_window { 342 display: none; 343 position: relative; 344 left: 0px; 345 top: 0px; 346 /*border: solid #627173 1px; */ 347 padding: 10px; 348 background-color: #E6E6D6; 349 font-family: "Lucida Console", "Courier New", Courier, monospace; 350 text-align: left; 351 font-size: 8pt; 352 width: 500px; 353 } 354 355 } 356 /* -- report ------------------------------------------------------------------------ */ 357 #show_detail_line { 358 margin-top: 3ex; 359 margin-bottom: 1ex; 360 } 361 #result_table { 362 width: 80%; 363 border-collapse: collapse; 364 border: 1px solid #777; 365 } 366 #header_row { 367 font-weight: bold; 368 color: white; 369 background-color: #777; 370 } 371 #result_table td { 372 border: 1px solid #777; 373 padding: 2px; 374 } 375 #total_row { font-weight: bold; } 376 .passClass { background-color: #6c6; } 377 .failClass { background-color: #c60; } 378 .errorClass { background-color: #c00; } 379 .passCase { color: #6c6; } 380 .failCase { color: #c60; font-weight: bold; } 381 .errorCase { color: #c00; font-weight: bold; } 382 .hiddenRow { display: none; } 383 .testcase { margin-left: 2em; } 384 385 386 /* -- ending ---------------------------------------------------------------------- */ 387 #ending { 388 } 389 390 </style> 391 """ 392 393 394 395 # ------------------------------------------------------------------------ 396 # Heading 397 # 398 399 HEADING_TMPL = """<div class='heading'> 400 <h1>%(title)s</h1> 401 %(parameters)s 402 <p class='description'>%(description)s</p> 403 </div> 404 405 """ # variables: (title, parameters, description) 406 407 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> 408 """ # variables: (name, value) 409 410 411 412 # ------------------------------------------------------------------------ 413 # Report 414 # 415 416 REPORT_TMPL = """ 417 <p id='show_detail_line'>Show 418 <a href='javascript:showCase(0)'>Summary</a> 419 <a href='javascript:showCase(1)'>Failed</a> 420 <a href='javascript:showCase(2)'>All</a> 421 </p> 422 <table id='result_table'> 423 <colgroup> 424 <col align='left' /> 425 <col align='right' /> 426 <col align='right' /> 427 <col align='right' /> 428 <col align='right' /> 429 <col align='right' /> 430 </colgroup> 431 <tr id='header_row'> 432 <td>Test Group/Test case</td> 433 <td>Count</td> 434 <td>Pass</td> 435 <td>Fail</td> 436 <td>Error</td> 437 <td>View</td> 438 </tr> 439 %(test_list)s 440 <tr id='total_row'> 441 <td>Total</td> 442 <td>%(count)s</td> 443 <td>%(Pass)s</td> 444 <td>%(fail)s</td> 445 <td>%(error)s</td> 446 <td>&nbsp;</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'>&nbsp;</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('\n') 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('\n') 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('\n') 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, '\nTime 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("\n")[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 \n 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 \n 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) 825