python3也支持的HTMLTestRunner

Posted jayson-0425

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python3也支持的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 io
 95 import sys
 96 import time
 97 import unittest
 98 from xml.sax import saxutils
 99 
100 
101 # ------------------------------------------------------------------------
102 # The redirectors below are used to capture output during testing. Output
103 # sent to sys.stdout and sys.stderr are automatically captured. However
104 # in some cases sys.stdout is already cached before HTMLTestRunner is
105 # invoked (e.g. calling logging.basicConfig). In order to capture those
106 # output, use the redirectors for the cached stream.
107 #
108 # e.g.
109 #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
110 #   >>>
111 
112 class OutputRedirector(object):
113     """ Wrapper to redirect stdout or stderr """
114     def __init__(self, fp):
115         self.fp = fp
116 
117     def write(self, s):
118         self.fp.write(s)
119 
120     def writelines(self, lines):
121         self.fp.writelines(lines)
122 
123     def flush(self):
124         self.fp.flush()
125 
126 stdout_redirector = OutputRedirector(sys.stdout)
127 stderr_redirector = OutputRedirector(sys.stderr)
128 
129 
130 
131 # ----------------------------------------------------------------------
132 # Template
133 
134 class Template_mixin(object):
135     """
136     Define a HTML template for report customerization and generation.
137 
138     Overall structure of an HTML report
139 
140     HTML
141     +------------------------+
142     |<html>                  |
143     |  <head>                |
144     |                        |
145     |   STYLESHEET           |
146     |   +----------------+   |
147     |   |                |   |
148     |   +----------------+   |
149     |                        |
150     |  </head>               |
151     |                        |
152     |  <body>                |
153     |                        |
154     |   HEADING              |
155     |   +----------------+   |
156     |   |                |   |
157     |   +----------------+   |
158     |                        |
159     |   REPORT               |
160     |   +----------------+   |
161     |   |                |   |
162     |   +----------------+   |
163     |                        |
164     |   ENDING               |
165     |   +----------------+   |
166     |   |                |   |
167     |   +----------------+   |
168     |                        |
169     |  </body>               |
170     |</html>                 |
171     +------------------------+
172     """
173 
174     STATUS = {
175     0: pass,
176     1: fail,
177     2: error,
178     }
179 
180     DEFAULT_TITLE = Unit Test Report
181     DEFAULT_DESCRIPTION = ‘‘
182 
183     # ------------------------------------------------------------------------
184     # HTML Template
185 
186     HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
187 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
188 <html xmlns="http://www.w3.org/1999/xhtml">
189 <head>
190     <title>%(title)s</title>
191     <meta name="generator" content="%(generator)s"/>
192     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
193     %(stylesheet)s
194 </head>
195 <body>
196 <script language="javascript" type="text/javascript"><!--
197 output_list = Array();
198 
199 /* level - 0:Summary; 1:Failed; 2:All */
200 function showCase(level) {
201     trs = document.getElementsByTagName("tr");
202     for (var i = 0; i < trs.length; i++) {
203         tr = trs[i];
204         id = tr.id;
205         if (id.substr(0,2) == ‘ft‘) {
206             if (level < 1) {
207                 tr.className = ‘hiddenRow‘;
208             }
209             else {
210                 tr.className = ‘‘;
211             }
212         }
213         if (id.substr(0,2) == ‘pt‘) {
214             if (level > 1) {
215                 tr.className = ‘‘;
216             }
217             else {
218                 tr.className = ‘hiddenRow‘;
219             }
220         }
221     }
222 }
223 
224 
225 function showClassDetail(cid, count) {
226     var id_list = Array(count);
227     var toHide = 1;
228     for (var i = 0; i < count; i++) {
229         tid0 = ‘t‘ + cid.substr(1) + ‘.‘ + (i+1);
230         tid = ‘f‘ + tid0;
231         tr = document.getElementById(tid);
232         if (!tr) {
233             tid = ‘p‘ + tid0;
234             tr = document.getElementById(tid);
235         }
236         id_list[i] = tid;
237         if (tr.className) {
238             toHide = 0;
239         }
240     }
241     for (var i = 0; i < count; i++) {
242         tid = id_list[i];
243         if (toHide) {
244             document.getElementById(‘div_‘+tid).style.display = ‘none‘
245             document.getElementById(tid).className = ‘hiddenRow‘;
246         }
247         else {
248             document.getElementById(tid).className = ‘‘;
249         }
250     }
251 }
252 
253 
254 function showTestDetail(div_id){
255     var details_div = document.getElementById(div_id)
256     var displayState = details_div.style.display
257     // alert(displayState)
258     if (displayState != ‘block‘ ) {
259         displayState = ‘block‘
260         details_div.style.display = ‘block‘
261     }
262     else {
263         details_div.style.display = ‘none‘
264     }
265 }
266 
267 
268 function html_escape(s) {
269     s = s.replace(/&/g,‘&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("
");
284     d.write("<a href=‘javascript:window.close()‘>close</a>
");
285     d.write("</pre>
");
286     d.close();
287 }
288 */
289 --></script>
290 
291 %(heading)s
292 %(report)s
293 %(ending)s
294 
295 </body>
296 </html>
297 """
298     # variables: (title, generator, stylesheet, heading, report, ending)
299 
300 
301     # ------------------------------------------------------------------------
302     # Stylesheet
303     #
304     # alternatively use a <link> for external style sheet, e.g.
305     #   <link rel="stylesheet" href="$url" type="text/css">
306 
307     STYLESHEET_TMPL = """
308 <style type="text/css" media="screen">
309 body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
310 table       { font-size: 100%; }
311 pre         { }
312 
313 /* -- heading ---------------------------------------------------------------------- */
314 h1 {
315     font-size: 16pt;
316     color: gray;
317 }
318 .heading {
319     margin-top: 0ex;
320     margin-bottom: 1ex;
321 }
322 
323 .heading .attribute {
324     margin-top: 1ex;
325     margin-bottom: 0;
326 }
327 
328 .heading .description {
329     margin-top: 4ex;
330     margin-bottom: 6ex;
331 }
332 
333 /* -- css div popup ------------------------------------------------------------------------ */
334 a.popup_link {
335 }
336 
337 a.popup_link:hover {
338     color: red;
339 }
340 
341 .popup_window {
342     display: none;
343     position: relative;
344     left: 0px;
345     top: 0px;
346     /*border: solid #627173 1px; */
347     padding: 10px;
348     background-color: #E6E6D6;
349     font-family: "Lucida Console", "Courier New", Courier, monospace;
350     text-align: left;
351     font-size: 8pt;
352     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 = io.StringIO()
540         stdout_redirector.fp = self.outputBuffer
541         stderr_redirector.fp = self.outputBuffer
542         self.stdout0 = sys.stdout
543         self.stderr0 = sys.stderr
544         sys.stdout = stdout_redirector
545         sys.stderr = stderr_redirector
546 
547 
548     def complete_output(self):
549         """
550         Disconnect output redirection and return buffer.
551         Safe to call multiple times.
552         """
553         if self.stdout0:
554             sys.stdout = self.stdout0
555             sys.stderr = self.stderr0
556             self.stdout0 = None
557             self.stderr0 = None
558         return self.outputBuffer.getvalue()
559 
560 
561     def stopTest(self, test):
562         # Usually one of addSuccess, addError or addFailure would have been called.
563         # But there are some path in unittest that would bypass this.
564         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
565         self.complete_output()
566 
567 
568     def addSuccess(self, test):
569         self.success_count += 1
570         TestResult.addSuccess(self, test)
571         output = self.complete_output()
572         self.result.append((0, test, output, ‘‘))
573         if self.verbosity > 1:
574             sys.stderr.write(ok )
575             sys.stderr.write(str(test))
576             sys.stderr.write(
)
577         else:
578             sys.stderr.write(.)
579 
580     def addError(self, test, err):
581         self.error_count += 1
582         TestResult.addError(self, test, err)
583         _, _exc_str = self.errors[-1]
584         output = self.complete_output()
585         self.result.append((2, test, output, _exc_str))
586         if self.verbosity > 1:
587             sys.stderr.write(E  )
588             sys.stderr.write(str(test))
589             sys.stderr.write(
)
590         else:
591             sys.stderr.write(E)
592 
593     def addFailure(self, test, err):
594         self.failure_count += 1
595         TestResult.addFailure(self, test, err)
596         _, _exc_str = self.failures[-1]
597         output = self.complete_output()
598         self.result.append((1, test, output, _exc_str))
599         if self.verbosity > 1:
600             sys.stderr.write(F  )
601             sys.stderr.write(str(test))
602             sys.stderr.write(
)
603         else:
604             sys.stderr.write(F)
605 
606 class HTMLTestRunner(Template_mixin):
607     """
608     """
609     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
610         self.stream = stream
611         self.verbosity = verbosity
612         if title is None:
613             self.title = self.DEFAULT_TITLE
614         else:
615             self.title = title
616         if description is None:
617             self.description = self.DEFAULT_DESCRIPTION
618         else:
619             self.description = description
620 
621         self.startTime = datetime.datetime.now()
622 
623 
624     def run(self, test):
625         "Run the given test case or test suite."
626         result = _TestResult(self.verbosity)
627         test(result)
628         self.stopTime = datetime.datetime.now()
629         self.generateReport(test, result)
630         print (sys.stderr, 
Time Elapsed: %s % (self.stopTime-self.startTime))
631         return result
632 
633 
634     def sortResult(self, result_list):
635         # unittest does not seems to run in any particular order.
636         # Here at least we want to group them together by class.
637         rmap = {}
638         classes = []
639         for n,t,o,e in result_list:
640             cls = t.__class__
641             if not cls in rmap:
642                 rmap[cls] = []
643                 classes.append(cls)
644             rmap[cls].append((n,t,o,e))
645         r = [(cls, rmap[cls]) for cls in classes]
646         return r
647 
648 
649     def getReportAttributes(self, result):
650         """
651         Return report attributes as a list of (name, value).
652         Override this to add custom attributes.
653         """
654         startTime = str(self.startTime)[:19]
655         duration = str(self.stopTime - self.startTime)
656         status = []
657         if result.success_count: status.append(Pass %s    % result.success_count)
658         if result.failure_count: status.append(Failure %s % result.failure_count)
659         if result.error_count:   status.append(Error %s   % result.error_count  )
660         if status:
661             status =  .join(status)
662         else:
663             status = none
664         return [
665             (Start Time, startTime),
666             (Duration, duration),
667             (Status, status),
668         ]
669 
670 
671     def generateReport(self, test, result):
672         report_attrs = self.getReportAttributes(result)
673         generator = HTMLTestRunner %s % __version__
674         stylesheet = self._generate_stylesheet()
675         heading = self._generate_heading(report_attrs)
676         report = self._generate_report(result)
677         ending = self._generate_ending()
678         output = self.HTML_TMPL % dict(
679             title = saxutils.escape(self.title),
680             generator = generator,
681             stylesheet = stylesheet,
682             heading = heading,
683             report = report,
684             ending = ending,
685         )
686         self.stream.write(output.encode(utf8))
687 
688 
689     def _generate_stylesheet(self):
690         return self.STYLESHEET_TMPL
691 
692 
693     def _generate_heading(self, report_attrs):
694         a_lines = []
695         for name, value in report_attrs:
696             line = self.HEADING_ATTRIBUTE_TMPL % dict(
697                     name = saxutils.escape(name),
698                     value = saxutils.escape(value),
699                 )
700             a_lines.append(line)
701         heading = self.HEADING_TMPL % dict(
702             title = saxutils.escape(self.title),
703             parameters = ‘‘.join(a_lines),
704             description = saxutils.escape(self.description),
705         )
706         return heading
707 
708 
709     def _generate_report(self, result):
710         rows = []
711         sortedResult = self.sortResult(result.result)
712         for cid, (cls, cls_results) in enumerate(sortedResult):
713             # subtotal for a class
714             np = nf = ne = 0
715             for n,t,o,e in cls_results:
716                 if n == 0: np += 1
717                 elif n == 1: nf += 1
718                 else: ne += 1
719 
720             # format class description
721             if cls.__module__ == "__main__":
722                 name = cls.__name__
723             else:
724                 name = "%s.%s" % (cls.__module__, cls.__name__)
725             doc = cls.__doc__ and cls.__doc__.split("
")[0] or ""
726             desc = doc and %s: %s % (name, doc) or name
727 
728             row = self.REPORT_CLASS_TMPL % dict(
729                 style = ne > 0 and errorClass or nf > 0 and failClass or passClass,
730                 desc = desc,
731                 count = np+nf+ne,
732                 Pass = np,
733                 fail = nf,
734                 error = ne,
735                 cid = c%s % (cid+1),
736             )
737             rows.append(row)
738 
739             for tid, (n,t,o,e) in enumerate(cls_results):
740                 self._generate_report_test(rows, cid, tid, n, t, o, e)
741 
742         report = self.REPORT_TMPL % dict(
743             test_list = ‘‘.join(rows),
744             count = str(result.success_count+result.failure_count+result.error_count),
745             Pass = str(result.success_count),
746             fail = str(result.failure_count),
747             error = str(result.error_count),
748         )
749         return report
750 
751 
752     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
753         # e.g. ‘pt1.1‘, ‘ft1.1‘, etc
754         has_output = bool(o or e)
755         tid = (n == 0 and p or f) + t%s.%s % (cid+1,tid+1)
756         name = t.id().split(.)[-1]
757         doc = t.shortDescription() or ""
758         desc = doc and (%s: %s % (name, doc)) or name
759         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
760 
761         # o and e should be byte string because they are collected from stdout and stderr?
762         if isinstance(o,str):
763             # TODO: some problem with ‘string_escape‘: it escape 
 and mess up formating
764             # uo = unicode(o.encode(‘string_escape‘))
765             uo = e
766         else:
767             uo = o
768         if isinstance(e,str):
769             # TODO: some problem with ‘string_escape‘: it escape 
 and mess up formating
770             # ue = unicode(e.encode(‘string_escape‘))
771             ue = e
772         else:
773             ue = e
774 
775         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
776             id = tid,
777             output = saxutils.escape(uo+ue),
778         )
779 
780         row = tmpl % dict(
781             tid = tid,
782             Class = (n == 0 and hiddenRow or none),
783             style = n == 2 and errorCase or (n == 1 and failCase or none),
784             desc = desc,
785             script = script,
786             status = self.STATUS[n],
787         )
788         rows.append(row)
789         if not has_output:
790             return
791 
792     def _generate_ending(self):
793         return self.ENDING_TMPL
794 
795 
796 ##############################################################################
797 # Facilities for running tests from the command line
798 ##############################################################################
799 
800 # Note: Reuse unittest.TestProgram to launch test. In the future we may
801 # build our own launcher to support more specific command line
802 # parameters like test title, CSS, etc.
803 class TestProgram(unittest.TestProgram):
804     """
805     A variation of the unittest.TestProgram. Please refer to the base
806     class for command line parameters.
807     """
808     def runTests(self):
809         # Pick HTMLTestRunner as the default test runner.
810         # base class‘s testRunner parameter is not useful because it means
811         # we have to instantiate HTMLTestRunner before we know self.verbosity.
812         if self.testRunner is None:
813             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
814         unittest.TestProgram.runTests(self)
815 
816 main = TestProgram
817 
818 ##############################################################################
819 # Executing this module from the command line
820 ##############################################################################
821 
822 if __name__ == "__main__":
823     main(module=None)

 

以上是关于python3也支持的HTMLTestRunner的主要内容,如果未能解决你的问题,请参考以下文章

Python3之HTMLTestRunner测试报告美化

selenium3.4.3 + python3.6 + HTMLTestRunner0.8.0

python3 中引用 HTMLTestRunner.py 模块的注意事项

HTMLTestRunner修改Python3的版本

HTMLTestRunner修改成Python3版本

python3 htmltestrunner 怎么判断用例失败