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,'&amp;');
 336     s = s.replace(/</g,'&lt;');
 337     s = s.replace(/>/g,'&gt;');
 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>&nbsp;</td>
 655     <td>&nbsp;</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>&nbsp;</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'>&nbsp;</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)

 

posted @ 2021-01-19 20:25  Juno3550  阅读(2252)  评论(0编辑  收藏  举报