Unittest - HTMLTestRunner.py(新版)
报告样式效果:
报告源码:HTMLTestRunner.py
1 """ 2 A TestRunner for use with the Python unit testing framework. It 3 generates a HTML report to show the result at a glance. 4 5 ------------------------------------------------------------------------ 6 Copyright (c) 2004-2020, Wai Yip Tung 7 All rights reserved. 8 Redistribution and use in source and binary forms, with or without 9 modification, are permitted provided that the following conditions are 10 met: 11 * Redistributions of source code must retain the above copyright notice, 12 this list of conditions and the following disclaimer. 13 * Redistributions in binary form must reproduce the above copyright 14 notice, this list of conditions and the following disclaimer in the 15 documentation and/or other materials provided with the distribution. 16 * Neither the name Wai Yip Tung nor the names of its contributors may be 17 used to endorse or promote products derived from this software without 18 specific prior written permission. 19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 20 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 22 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 23 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 """ 31 32 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 33 34 __author__ = "Wai Yip Tung , bugmaster" 35 __version__ = "0.9.0" 36 37 """ 38 Change History 39 40 Version 0.9.0 41 * Increased repeat execution 42 * Added failure screenshots 43 44 Version 0.8.2 45 * Show output inline instead of popup window (Viorel Lupu). 46 47 Version in 0.8.1 48 * Validated XHTML (Wolfgang Borgert). 49 * Added description of test classes and test cases. 50 51 Version in 0.8.0 52 * Define Template_mixin class for customization. 53 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 54 55 Version in 0.7.1 56 * Back port to Python 2.3 (Frank Horowitz). 57 * Fix missing scroll bars in detail log (Podi). 58 """ 59 60 # TODO: color stderr 61 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 62 63 import datetime 64 import io 65 import sys 66 import time 67 import copy 68 import unittest 69 from xml.sax import saxutils 70 71 72 # ------------------------------------------------------------------------ 73 # The redirectors below are used to capture output during testing. Output 74 # sent to sys.stdout and sys.stderr are automatically captured. However 75 # in some cases sys.stdout is already cached before HTMLTestRunner is 76 # invoked (e.g. calling logging.basicConfig). In order to capture those 77 # output, use the redirectors for the cached stream. 78 # 79 # e.g. 80 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 81 # >>> 82 83 class OutputRedirector(object): 84 """ Wrapper to redirect stdout or stderr """ 85 86 def __init__(self, fp): 87 self.fp = fp 88 89 def write(self, s): 90 self.fp.write(s) 91 92 def writelines(self, lines): 93 self.fp.writelines(lines) 94 95 def flush(self): 96 self.fp.flush() 97 98 99 stdout_redirector = OutputRedirector(sys.stdout) 100 stderr_redirector = OutputRedirector(sys.stderr) 101 102 103 # ---------------------------------------------------------------------- 104 # Template 105 106 class Template_mixin(object): 107 """ 108 Define a HTML template for report customerization and generation. 109 Overall structure of an HTML report 110 HTML 111 +------------------------+ 112 |<html> | 113 | <head> | 114 | | 115 | STYLESHEET | 116 | +----------------+ | 117 | | | | 118 | +----------------+ | 119 | | 120 | </head> | 121 | | 122 | <body> | 123 | | 124 | HEADING | 125 | +----------------+ | 126 | | | | 127 | +----------------+ | 128 | | 129 | REPORT | 130 | +----------------+ | 131 | | | | 132 | +----------------+ | 133 | | 134 | ENDING | 135 | +----------------+ | 136 | | | | 137 | +----------------+ | 138 | | 139 | </body> | 140 |</html> | 141 +------------------------+ 142 """ 143 144 STATUS = { 145 0: 'pass', 146 1: 'fail', 147 2: 'error', 148 3: 'skip', 149 } 150 151 DEFAULT_TITLE = 'Unit Test Report' 152 DEFAULT_DESCRIPTION = '' 153 154 # ------------------------------------------------------------------------ 155 # HTML Template 156 157 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 158 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 159 <html xmlns="http://www.w3.org/1999/xhtml"> 160 <head> 161 <title>%(title)s</title> 162 <meta name="generator" content="%(generator)s"/> 163 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 164 <script src="https://lib.baomitu.com/jquery/3.5.1/jquery.min.js"></script> 165 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script> 166 <script src="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script> 167 <script src="http://apps.bdimg.com/libs/Chart.js/0.2.0/Chart.min.js"></script> 168 <link rel="stylesheet" href="http://img.itest.info/seldom.css"> 169 170 %(stylesheet)s 171 </head> 172 <body> 173 <script language="javascript" type="text/javascript"> 174 175 function show_img(obj) { 176 var obj1 = obj.nextElementSibling 177 obj1.style.display='block' 178 var index = 0;//每张图片的下标, 179 var len = obj1.getElementsByTagName('img').length; 180 var imgyuan = obj1.getElementsByClassName('imgyuan')[0] 181 //var start=setInterval(autoPlay,500); 182 obj1.onmouseover=function(){//当鼠标光标停在图片上,则停止轮播 183 clearInterval(start); 184 } 185 obj1.onmouseout=function(){//当鼠标光标停在图片上,则开始轮播 186 start=setInterval(autoPlay,1000); 187 } 188 for (var i = 0; i < len; i++) { 189 var font = document.createElement('font') 190 imgyuan.appendChild(font) 191 } 192 var lis = obj1.getElementsByTagName('font');//得到所有圆圈 193 changeImg(0) 194 var funny = function (i) { 195 lis[i].onmouseover = function () { 196 index=i 197 changeImg(i) 198 } 199 } 200 for (var i = 0; i < lis.length; i++) { 201 funny(i); 202 } 203 204 function autoPlay(){ 205 if(index>len-1){ 206 index=0; 207 clearInterval(start); //运行一轮后停止 208 } 209 changeImg(index++); 210 } 211 imgyuan.style.width= 25*len +"px"; 212 //对应圆圈和图片同步 213 function changeImg(index) { 214 var list = obj1.getElementsByTagName('img'); 215 var list1 = obj1.getElementsByTagName('font'); 216 for (i = 0; i < list.length; i++) { 217 list[i].style.display = 'none'; 218 list1[i].style.backgroundColor = 'white'; 219 } 220 list[index].style.display = 'block'; 221 list1[index].style.backgroundColor = 'red'; 222 } 223 } 224 225 function hide_img(obj){ 226 obj.parentElement.style.display = "none"; 227 obj.parentElement.getElementsByClassName('imgyuan')[0].innerHTML = ""; 228 } 229 230 output_list = Array(); 231 /* level - 0:Summary; 1:Failed; 2:Skip; 3:All */ 232 function showCase(level, channel) { 233 trs = document.getElementsByTagName("tr"); 234 for (var i = 0; i < trs.length; i++) { 235 tr = trs[i]; 236 id = tr.id; 237 if (["ft","pt","et","st"].indexOf(id.substr(0,2))!=-1){ 238 if ( level == 0 && id.substr(2,1) == channel ) { 239 tr.className = 'hiddenRow'; 240 } 241 } 242 if (id.substr(0,3) == 'pt'+ channel) { 243 if ( level == 1){ 244 tr.className = ''; 245 } 246 else if (level > 4 && id.substr(2,1) == channel ){ 247 tr.className = ''; 248 } 249 else { 250 tr.className = 'hiddenRow'; 251 } 252 } 253 if (id.substr(0,3) == 'ft'+channel) { 254 if (level == 2) { 255 tr.className = ''; 256 } 257 else if (level > 4 && id.substr(2,1) == channel ){ 258 tr.className = ''; 259 } 260 else { 261 tr.className = 'hiddenRow'; 262 } 263 } 264 if (id.substr(0,3) == 'et'+channel) { 265 if (level == 3) { 266 tr.className = ''; 267 } 268 else if (level > 4 && id.substr(2,1) == channel ){ 269 tr.className = ''; 270 } 271 else { 272 tr.className = 'hiddenRow'; 273 } 274 } 275 if (id.substr(0,3) == 'st'+channel) { 276 if (level == 4) { 277 tr.className = ''; 278 } 279 else if (level > 4 && id.substr(2,1) == channel ){ 280 tr.className = ''; 281 } 282 else { 283 tr.className = 'hiddenRow'; 284 } 285 } 286 } 287 } 288 function showClassDetail(cid, count) { 289 var id_list = Array(count); 290 var toHide = 1; 291 for (var i = 0; i < count; i++) { 292 tid0 = 't' + cid.substr(1) + '.' + (i+1); 293 tid = 'f' + tid0; 294 tr = document.getElementById(tid); 295 if (!tr) { 296 tid = 'p' + tid0; 297 tr = document.getElementById(tid); 298 } 299 if (!tr) { 300 tid = 'e' + tid0; 301 tr = document.getElementById(tid); 302 } 303 if (!tr) { 304 tid = 's' + tid0; 305 tr = document.getElementById(tid); 306 } 307 id_list[i] = tid; 308 if (tr.className) { 309 toHide = 0; 310 } 311 } 312 for (var i = 0; i < count; i++) { 313 tid = id_list[i]; 314 if (toHide) { 315 document.getElementById(tid).className = 'hiddenRow'; 316 } 317 else { 318 document.getElementById(tid).className = ''; 319 } 320 } 321 } 322 function showTestDetail(div_id){ 323 var details_div = document.getElementById(div_id) 324 var displayState = details_div.style.display 325 // alert(displayState) 326 if (displayState != 'block' ) { 327 displayState = 'block' 328 details_div.style.display = 'block' 329 } 330 else { 331 details_div.style.display = 'none' 332 } 333 } 334 function html_escape(s) { 335 s = s.replace(/&/g,'&'); 336 s = s.replace(/</g,'<'); 337 s = s.replace(/>/g,'>'); 338 return s; 339 } 340 /* obsoleted by detail in <div> 341 function showOutput(id, name) { 342 var w = window.open("", //url 343 name, 344 "resizable,scrollbars,status,width=800,height=450"); 345 d = w.document; 346 d.write("<pre>"); 347 d.write(html_escape(output_list[id])); 348 d.write("\n"); 349 d.write("<a href='javascript:window.close()'>close</a>\n"); 350 d.write("</pre>\n"); 351 d.close(); 352 } 353 */ 354 </script> 355 %(heading)s 356 %(report)s 357 %(ending)s 358 %(chart_script)s 359 </body> 360 </html> 361 """ 362 # variables: (title, generator, stylesheet, heading, report, ending) 363 364 # ------------------------------------------------------------------------ 365 # Stylesheet 366 # 367 # alternatively use a <link> for external style sheet, e.g. 368 # <link rel="stylesheet" href="$url" type="text/css"> 369 370 STYLESHEET_TMPL = """ 371 <style type="text/css" media="screen"> 372 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } 373 table { font-size: 100%; } 374 pre { } 375 /* -- heading ---------------------------------------------------------------------- */ 376 h1 { 377 font-size: 16pt; 378 color: gray; 379 } 380 .heading { 381 margin-top: 20px; 382 margin-bottom: 1ex; 383 margin-left: 10px; 384 margin-right: 10px; 385 width: 23%; 386 float: left; 387 padding-top: 10px; 388 padding-left: 10px; 389 padding-bottom: 10px; 390 padding-right: 10px; 391 box-shadow:0px 0px 5px #000; 392 } 393 .heading .attribute { 394 margin-top: 1ex; 395 margin-bottom: 0; 396 } 397 .heading .description { 398 margin-top: 4ex; 399 margin-bottom: 6ex; 400 } 401 /* -- css div popup ------------------------------------------------------------------------ */ 402 a.popup_link { 403 } 404 a.popup_link:hover { 405 color: red; 406 } 407 .popup_window { 408 display: none; 409 position: relative; 410 left: 0px; 411 top: 0px; 412 /*border: solid #627173 1px; */ 413 font-family: "Lucida Console", "Courier New", Courier, monospace; 414 text-align: left; 415 font-size: 12pt; 416 width: 500px; 417 } 418 } 419 /* -- report ------------------------------------------------------------------------ */ 420 #show_detail_line { 421 margin-top: 3ex; 422 margin-bottom: 1ex; 423 margin-left: 10px; 424 } 425 426 #header_row { 427 font-weight: bold; 428 color: #606060; 429 background-color: #f5f5f5; 430 border-top-width: 10px; 431 border-color: #d6e9c6; 432 font-size: 15px; 433 } 434 435 #total_row { font-weight: bold; background-color: #dee2e6;} 436 .passClass { background-color: #d6e9c6; } 437 .failClass { background-color: #faebcc; } 438 .errorClass { background-color: #ebccd1; } 439 .passCase { color: #28a745; font-weight: bold;} 440 .failCase { color: #c60; font-weight: bold; } 441 .errorCase { color: #c00; font-weight: bold; } 442 .hiddenRow { display: none; } 443 .none {color: #009900 } 444 .testcase { margin-left: 2em; } 445 /* -- ending ---------------------------------------------------------------------- */ 446 #ending { 447 } 448 /* -- chars ---------------------------------------------------------------------- */ 449 .testChars {width: 900px;margin-left: 0px;} 450 .error-color { 451 color: #fff; 452 background-color: #f44455; 453 border-color: #f44455; 454 } 455 .pass-color { 456 color: #fff; 457 background-color: #5fc27e; 458 border-color: #5fc27e; 459 } 460 .fail-color { 461 color: #fff; 462 background-color: #fcc100; 463 border-color: #fcc100; 464 } 465 .skip-color { 466 color: #fff; 467 background-color: #6c757d; 468 border-color: #6c757d; 469 } 470 471 /* -- screenshots ---------------------------------------------------------------------- */ 472 .img{ 473 height: 100%; 474 border-collapse: collapse; 475 } 476 .screenshots { 477 z-index: 100; 478 position:fixed; 479 height: 80%; 480 left: 50%; 481 top: 50%; 482 transform: translate(-50%,-50%); 483 display: none; 484 box-shadow:1px 2px 20px #333333; 485 } 486 .imgyuan{ 487 height: 20px; 488 border-radius: 12px; 489 background-color: red; 490 padding-left: 13px; 491 margin: 0 auto; 492 position: relative; 493 top: -40px; 494 background-color: rgba(1, 150, 0, 0.3); 495 } 496 .imgyuan font{ 497 border:1px solid white; 498 width:11px; 499 height:11px; 500 border-radius:50%; 501 margin-right: 9px; 502 margin-top: 4px; 503 display: block; 504 float: left; 505 background-color: white; 506 } 507 508 .close_shots { 509 background-image: url(data:image/png;base64,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); 510 background-size: 22px 22px; 511 -moz-background-size: 22px 22px; 512 background-repeat: no-repeat; 513 position: absolute; 514 top: 5px; 515 right: 5px; 516 height: 22px; 517 z-index: 99; 518 width: 22px; 519 ox-shadow:1px 2px 5px #333333; 520 } 521 522 </style> 523 """ 524 525 # ------------------------------------------------------------------------ 526 # Heading 527 # 528 529 HEADING_TMPL = """ 530 <nav class="navbar navbar-expand navbar-light bg-white"> 531 <a class="sidebar-toggle d-flex mr-2"> 532 <i class="hamburger align-self-center"></i> 533 </a> 534 <h1 style="margin-bottom: 0px;">seldom</h1> 535 <div class="navbar-collapse collapse"> 536 <ul class="navbar-nav ml-auto"> 537 <h3 style="float: right;">%(title)s</h3> 538 </ul> 539 </div> 540 </nav> 541 <div style="height: 260px; margin-top: 20px;"> 542 <div class="col-12 col-lg-5 col-xl-3 d-flex" style="float:left"> 543 <div class='card flex-fill'> 544 <div class="card-body my-2"> 545 <table class="table my-0"> 546 <tbody> 547 %(parameters)s 548 <tr><td>Description:</td><td class="text-right">%(description)s</td></tr> 549 </tbody> 550 </table> 551 </div> 552 </div> 553 </div> 554 555 <div style="float:left; margin-left: 10px; margin-top: 20px;"> 556 <p> Test Case Pie charts </p> 557 <h2 class="d-flex align-items-center mb-0 font-weight-light pass-color">%(pass_count)s</h2> 558 <a>PASSED</a><br> 559 <h2 class="d-flex align-items-center mb-0 font-weight-light fail-color">%(fail_count)s</h2> 560 <a>FAILED</a> 561 <h2 class="d-flex align-items-center mb-0 font-weight-light error-color">%(error_count)s</h2> 562 <a>ERRORS</a><br> 563 <h2 class="d-flex align-items-center mb-0 font-weight-light skip-color">%(skip_count)s</h2> 564 <a>SKIPED</a><br> 565 </div> 566 <div class="testChars"> 567 <canvas id="myChart" width="250" height="250"></canvas> 568 </div> 569 570 </div> 571 """ # variables: (title, parameters, description) 572 573 # ------------------------------------------------------------------------ 574 # Pie chart 575 # 576 577 ECHARTS_SCRIPT = """ 578 <script type="text/javascript"> 579 var data = [ 580 { 581 value: %(error)s, 582 color: "#f44455", 583 label: "Error", 584 labelColor: 'white', 585 labelFontSize: '16' 586 }, 587 { 588 value : %(fail)s, 589 color : "#fcc100", 590 label: "Fail", 591 labelColor: 'white', 592 labelFontSize: '16' 593 }, 594 { 595 value : %(Pass)s, 596 color : "#5fc27e", 597 label : "Pass", 598 labelColor: 'white', 599 labelFontSize: '16' 600 }, 601 { 602 value : %(skip)s, 603 color : "#6c757d", 604 label : "skip", 605 labelColor: 'white', 606 labelFontSize: '16' 607 } 608 ] 609 var newopts = { 610 animationSteps: 100, 611 animationEasing: 'easeInOutQuart', 612 } 613 //Get the context of the canvas element we want to select 614 var ctx = document.getElementById("myChart").getContext("2d"); 615 var myNewChart = new Chart(ctx).Pie(data,newopts); 616 </script> 617 """ 618 619 HEADING_ATTRIBUTE_TMPL = """<tr><td>%(name)s:</td><td class="text-right">%(value)s</td></tr> 620 """ # variables: (name, value) 621 622 # ------------------------------------------------------------------------ 623 # Report 624 # 625 626 REPORT_TMPL = """ 627 <p id='show_detail_line' style="margin-left: 10px; margin-top: 30px;"> 628 <a href='javascript:showCase(0, %(channel)s)' class="btn btn-dark btn-sm">Summary</a> 629 <a href='javascript:showCase(1, %(channel)s)' class="btn btn-success btn-sm">Pass</a> 630 <a href='javascript:showCase(2, %(channel)s)' class="btn btn-warning btn-sm">Failed</a> 631 <a href='javascript:showCase(3, %(channel)s)' class="btn btn-danger btn-sm">Error</a> 632 <a href='javascript:showCase(4, %(channel)s)' class="btn btn-light btn-sm">Skip</a> 633 <a href='javascript:showCase(5, %(channel)s)' class="btn btn-info btn-sm">All</a> 634 </p> 635 <table class="table mb-0"> 636 <thead> 637 <tr id='header_row'> 638 <td>Test Group/Test case</td> 639 <td>Count</td> 640 <td>Pass</td> 641 <td>Fail</td> 642 <td>Error</td> 643 <td>View</td> 644 <td>Screenshots</td> 645 </tr> 646 </thead> 647 %(test_list)s 648 <tr id='total_row'> 649 <td>Total</td> 650 <td>%(count)s</td> 651 <td class="text text-success">%(Pass)s</td> 652 <td class="text text-danger">%(fail)s</td> 653 <td class="text text-warning">%(error)s</td> 654 <td> </td> 655 <td> </td> 656 </tr> 657 </table> 658 """ # variables: (test_list, count, Pass, fail, error) 659 660 REPORT_CLASS_TMPL = r""" 661 <tr class='%(style)s'> 662 <td>%(desc)s</td> 663 <td>%(count)s</td> 664 <td>%(Pass)s</td> 665 <td>%(fail)s</td> 666 <td>%(error)s</td> 667 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 668 <td> </td> 669 </tr> 670 """ # variables: (style, desc, count, Pass, fail, error, cid) 671 672 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 673 <tr id='%(tid)s' class='%(Class)s'> 674 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 675 <td colspan='5' align='center'> 676 <!--css div popup start--> 677 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 678 %(status)s</a> 679 <div id='div_%(tid)s' class="popup_window"> 680 <div style='text-align: right; color:red;cursor:pointer'> 681 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 682 [x]</a> 683 </div> 684 <pre> 685 %(script)s 686 </pre> 687 </div> 688 <!--css div popup end--> 689 </td> 690 <td>%(img)s</td> 691 </tr> 692 """ # variables: (tid, Class, style, desc, status) 693 694 REPORT_TEST_NO_OUTPUT_TMPL = r""" 695 <tr id='%(tid)s' class='%(Class)s'> 696 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 697 <td colspan='5' align='center'>%(status)s</td> 698 <td>%(img)s</td> 699 </tr> 700 """ # variables: (tid, Class, style, desc, status) 701 702 REPORT_TEST_OUTPUT_TMPL = r""" 703 %(id)s: %(output)s 704 """ # variables: (id, output) 705 706 IMG_TMPL = r""" 707 <a onfocus='this.blur();' href="javacript:void(0);" onclick="show_img(this)">show</a> 708 <div align="center" class="screenshots" style="display:none"> 709 <a class="close_shots" onclick="hide_img(this)"></a> 710 {imgs} 711 <div class="imgyuan"></div> 712 </div> 713 """ 714 # ------------------------------------------------------------------------ 715 # ENDING 716 # 717 718 ENDING_TMPL = """<div id='ending'> </div>""" 719 720 721 # -------------------- The end of the Template class ------------------- 722 723 724 TestResult = unittest.TestResult 725 726 727 class _TestResult(TestResult): 728 # note: _TestResult is a pure representation of results. 729 # It lacks the output and reporting ability compares to unittest._TextTestResult. 730 731 def __init__(self, verbosity=1, rerun=0, save_last_run=False): 732 TestResult.__init__(self) 733 self.stdout0 = None 734 self.stderr0 = None 735 self.success_count = 0 736 self.failure_count = 0 737 self.error_count = 0 738 self.skip_count = 0 739 self.verbosity = verbosity 740 self.rerun = rerun 741 self.save_last_run = save_last_run 742 self.status = 0 743 self.runs = 0 744 self.result = [] 745 746 def startTest(self, test): 747 test.imgs = getattr(test, "imgs", []) 748 # TestResult.startTest(self, test) 749 # just one buffer for both stdout and stderr 750 self.outputBuffer = io.StringIO() 751 stdout_redirector.fp = self.outputBuffer 752 stderr_redirector.fp = self.outputBuffer 753 self.stdout0 = sys.stdout 754 self.stderr0 = sys.stderr 755 sys.stdout = stdout_redirector 756 sys.stderr = stderr_redirector 757 758 def complete_output(self): 759 """ 760 Disconnect output redirection and return buffer. 761 Safe to call multiple times. 762 """ 763 if self.stdout0: 764 sys.stdout = self.stdout0 765 sys.stderr = self.stderr0 766 self.stdout0 = None 767 self.stderr0 = None 768 return self.outputBuffer.getvalue() 769 770 def stopTest(self, test): 771 # Usually one of addSuccess, addError or addFailure would have been called. 772 # But there are some path in unittest that would bypass this. 773 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 774 if self.rerun and self.rerun >= 1: 775 if self.status == 1: 776 self.runs += 1 777 if self.runs <= self.rerun: 778 if self.save_last_run: 779 t = self.result.pop(-1) 780 if t[0] == 1: 781 self.failure_count -= 1 782 else: 783 self.error_count -= 1 784 test = copy.copy(test) 785 sys.stderr.write("Retesting... ") 786 sys.stderr.write(str(test)) 787 sys.stderr.write('..%d \n' % self.runs) 788 doc = getattr(test, '_testMethodDoc', u"") or u'' 789 if doc.find('->rerun') != -1: 790 doc = doc[:doc.find('->rerun')] 791 desc = "%s->rerun:%d" % (doc, self.runs) 792 if isinstance(desc, str): 793 desc = desc 794 test._testMethodDoc = desc 795 test(self) 796 else: 797 self.status = 0 798 self.runs = 0 799 self.complete_output() 800 801 def addSuccess(self, test): 802 self.success_count += 1 803 self.status = 0 804 TestResult.addSuccess(self, test) 805 output = self.complete_output() 806 self.result.append((0, test, output, '')) 807 if self.verbosity > 1: 808 sys.stderr.write('ok ') 809 sys.stderr.write(str(test)) 810 sys.stderr.write('\n') 811 else: 812 sys.stderr.write('.' + str(self.success_count)) 813 814 def addError(self, test, err): 815 self.error_count += 1 816 self.status = 1 817 TestResult.addError(self, test, err) 818 _, _exc_str = self.errors[-1] 819 output = self.complete_output() 820 self.result.append((2, test, output, _exc_str)) 821 if not getattr(test, "driver", ""): 822 pass 823 else: 824 try: 825 driver = getattr(test, "driver") 826 test.imgs.append(driver.get_screenshot_as_base64()) 827 except BaseException: 828 pass 829 if self.verbosity > 1: 830 sys.stderr.write('E ') 831 sys.stderr.write(str(test)) 832 sys.stderr.write('\n') 833 else: 834 sys.stderr.write('E') 835 836 def addFailure(self, test, err): 837 self.failure_count += 1 838 self.status = 1 839 TestResult.addFailure(self, test, err) 840 _, _exc_str = self.failures[-1] 841 output = self.complete_output() 842 self.result.append((1, test, output, _exc_str)) 843 if not getattr(test, "driver", ""): 844 pass 845 else: 846 try: 847 driver = getattr(test, "driver") 848 test.imgs.append(driver.get_screenshot_as_base64()) 849 except BaseException: 850 pass 851 if self.verbosity > 1: 852 sys.stderr.write('F ') 853 sys.stderr.write(str(test)) 854 sys.stderr.write('\n') 855 else: 856 sys.stderr.write('F') 857 858 def addSkip(self, test, reason): 859 self.skip_count += 1 860 self.status = 0 861 TestResult.addSkip(self, test, reason) 862 output = self.complete_output() 863 self.result.append((3, test, output, reason)) 864 if self.verbosity > 1: 865 sys.stderr.write('S') 866 sys.stderr.write(str(test)) 867 sys.stderr.write('\n') 868 else: 869 sys.stderr.write('S') 870 871 872 class HTMLTestRunner(Template_mixin): 873 """ 874 """ 875 876 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None, save_last_run=True): 877 self.stream = stream 878 self.verbosity = verbosity 879 self.save_last_run = save_last_run 880 self.run_times = 0 881 if title is None: 882 self.title = self.DEFAULT_TITLE 883 else: 884 self.title = title 885 if description is None: 886 self.description = self.DEFAULT_DESCRIPTION 887 else: 888 self.description = description 889 890 self.startTime = datetime.datetime.now() 891 892 def run(self, test, rerun=0, save_last_run=False): 893 """Run the given test case or test suite.""" 894 result = _TestResult(self.verbosity, rerun=rerun, save_last_run=save_last_run) 895 test(result) 896 self.stopTime = datetime.datetime.now() 897 self.run_times += 1 898 self.generateReport(test, result) 899 return result 900 901 def sortResult(self, result_list): 902 # unittest does not seems to run in any particular order. 903 # Here at least we want to group them together by class. 904 rmap = {} 905 classes = [] 906 for n, t, o, e in result_list: 907 cls = t.__class__ 908 if not cls in rmap: 909 rmap[cls] = [] 910 classes.append(cls) 911 rmap[cls].append((n, t, o, e)) 912 r = [(cls, rmap[cls]) for cls in classes] 913 return r 914 915 def getReportAttributes(self, result): 916 """ 917 Return report attributes as a list of (name, value). 918 Override this to add custom attributes. 919 """ 920 startTime = str(self.startTime)[:19] 921 duration = str(self.stopTime - self.startTime) 922 status = [] 923 if result.success_count: 924 status.append('Passed:%s' % result.success_count) 925 if result.failure_count: 926 status.append('Failed:%s' % result.failure_count) 927 if result.error_count: 928 status.append('Errors:%s' % result.error_count) 929 if result.skip_count: 930 status.append('Skiped:%s' % result.skip_count) 931 if status: 932 status = ' '.join(status) 933 else: 934 status = 'none' 935 result = { 936 "pass": result.success_count, 937 "fail": result.failure_count, 938 "error": result.error_count, 939 "skip": result.skip_count, 940 } 941 return [ 942 ('Start Time', startTime), 943 ('Duration', duration), 944 ('Status', status), 945 ('Result', result), 946 ] 947 948 def generateReport(self, test, result): 949 report_attrs = self.getReportAttributes(result) 950 generator = 'HTMLTestRunner %s' % __version__ 951 stylesheet = self._generate_stylesheet() 952 heading = self._generate_heading(report_attrs) 953 report = self._generate_report(result) 954 ending = self._generate_ending() 955 chart = self._generate_chart(result) 956 output = self.HTML_TMPL % dict( 957 title=saxutils.escape(self.title), 958 generator=generator, 959 stylesheet=stylesheet, 960 heading=heading, 961 report=report, 962 ending=ending, 963 chart_script=chart, 964 channel=self.run_times, 965 ) 966 self.stream.write(output.encode('utf8')) 967 968 def _generate_stylesheet(self): 969 return self.STYLESHEET_TMPL 970 971 def _generate_heading(self, report_attrs): 972 a_lines = [] 973 for name, value in report_attrs: 974 result = {} 975 if name == "Result": 976 result = value 977 else: 978 line = self.HEADING_ATTRIBUTE_TMPL % dict( 979 name=saxutils.escape(name), 980 value=saxutils.escape(value), 981 ) 982 a_lines.append(line) 983 heading = self.HEADING_TMPL % dict( 984 title=saxutils.escape(self.title), 985 parameters=''.join(a_lines), 986 description=saxutils.escape(self.description), 987 pass_count=saxutils.escape(str(result["pass"])), 988 fail_count=saxutils.escape(str(result["fail"])), 989 error_count=saxutils.escape(str(result["error"])), 990 skip_count=saxutils.escape(str(result["skip"])), 991 ) 992 return heading 993 994 def _generate_report(self, result): 995 rows = [] 996 sortedResult = self.sortResult(result.result) 997 for cid, (cls, cls_results) in enumerate(sortedResult): 998 # subtotal for a class 999 np = nf = ne = ns = 0 1000 for n, t, o, e in cls_results: 1001 if n == 0: 1002 np += 1 1003 elif n == 1: 1004 nf += 1 1005 elif n == 2: 1006 ne += 1 1007 else: 1008 ns += 1 1009 1010 # format class description 1011 if cls.__module__ == "__main__": 1012 name = cls.__name__ 1013 else: 1014 name = "%s.%s" % (cls.__module__, cls.__name__) 1015 doc = cls.__doc__ or "" 1016 desc = doc and '%s: %s' % (name, doc) or name 1017 1018 row = self.REPORT_CLASS_TMPL % dict( 1019 style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 1020 desc=desc, 1021 count=np + nf + ne, 1022 Pass=np, 1023 fail=nf, 1024 error=ne, 1025 cid='c%s.%s' % (self.run_times, cid + 1), 1026 ) 1027 rows.append(row) 1028 1029 for tid, (n, t, o, e) in enumerate(cls_results): 1030 print("o", o) 1031 self._generate_report_test(rows, cid, tid, n, t, o, e) 1032 1033 report = self.REPORT_TMPL % dict( 1034 test_list=''.join(rows), 1035 count=str(result.success_count + result.failure_count + result.error_count), 1036 Pass=str(result.success_count), 1037 fail=str(result.failure_count), 1038 error=str(result.error_count), 1039 skip=str(result.skip_count), 1040 total=str(result.success_count + result.failure_count + result.error_count), 1041 channel=str(self.run_times), 1042 ) 1043 return report 1044 1045 def _generate_chart(self, result): 1046 chart = self.ECHARTS_SCRIPT % dict( 1047 Pass=str(result.success_count), 1048 fail=str(result.failure_count), 1049 error=str(result.error_count), 1050 skip=str(result.skip_count), 1051 ) 1052 return chart 1053 1054 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 1055 # e.g. 'pt1.1', 'ft1.1','et1.1', 'st1.1' etc 1056 has_output = bool(o or e) 1057 if n == 0: 1058 tmp = "p" 1059 elif n == 1: 1060 tmp = "f" 1061 elif n == 2: 1062 tmp = "e" 1063 else: 1064 tmp = "s" 1065 tid = tmp + 't%d.%d.%d' % (self.run_times, cid + 1, tid + 1) 1066 # tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1) 1067 name = t.id().split('.')[-1] 1068 doc = t.shortDescription() or "" 1069 desc = doc and ('%s: %s' % (name, doc)) or name 1070 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 1071 1072 # o and e should be byte string because they are collected from stdout and stderr? 1073 if isinstance(o, str): 1074 # TODO: some problem with 'string_escape': it escape \n and mess up formating 1075 # uo = unicode(o.encode('string_escape')) 1076 uo = o 1077 else: 1078 uo = o 1079 if isinstance(e, str): 1080 # TODO: some problem with 'string_escape': it escape \n and mess up formating 1081 # ue = unicode(e.encode('string_escape')) 1082 ue = e 1083 else: 1084 ue = e 1085 1086 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 1087 id=tid, 1088 output=saxutils.escape(uo + ue), 1089 ) 1090 if getattr(t, 'imgs', []): 1091 # 判断截图列表,如果有则追加 1092 tmp = "" 1093 for i, img in enumerate(t.imgs): 1094 if i == 0: 1095 tmp += """<img src="data:image/jpg;base64,{}" style="display: block;" class="img"/>\n""".format(img) 1096 else: 1097 tmp += """<img src="data:image/jpg;base64,{}" style="display: none;" class="img"/>\n""".format(img) 1098 screenshots_html = self.IMG_TMPL.format(imgs=tmp) 1099 else: 1100 screenshots_html = """""" 1101 1102 row = tmpl % dict( 1103 tid=tid, 1104 Class=(n == 0 and 'hiddenRow' or 'none'), 1105 style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1106 desc=desc, 1107 script=script, 1108 status=self.STATUS[n], 1109 img=screenshots_html 1110 ) 1111 rows.append(row) 1112 if not has_output: 1113 return 1114 1115 def _generate_ending(self): 1116 return self.ENDING_TMPL 1117 1118 1119 ############################################################################## 1120 # Facilities for running tests from the command line 1121 ############################################################################## 1122 1123 # Note: Reuse unittest.TestProgram to launch test. In the future we may 1124 # build our own launcher to support more specific command line 1125 # parameters like test title, CSS, etc. 1126 class TestProgram(unittest.TestProgram): 1127 """ 1128 A variation of the unittest.TestProgram. Please refer to the base 1129 class for command line parameters. 1130 """ 1131 1132 def runTests(self): 1133 # Pick HTMLTestRunner as the default test runner. 1134 # base class's testRunner parameter is not useful because it means 1135 # we have to instantiate HTMLTestRunner before we know self.verbosity. 1136 if self.testRunner is None: 1137 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 1138 unittest.TestProgram.runTests(self) 1139 1140 1141 main = TestProgram 1142 1143 ############################################################################## 1144 # Executing this module from the command line 1145 ############################################################################## 1146 1147 if __name__ == "__main__": 1148 main(module=None)