1. HTMLTestRunner.py 代码(python3)如下:
python2: https://github.com/tungwaiyip/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.3"
69
70
71 """
72 Change History
73
74 Version 0.8.3
75 * Prevent crash on class or module-level exceptions (Darren Wurf).
76
77 Version 0.8.2
78 * Show output inline instead of popup window (Viorel Lupu).
79
80 Version in 0.8.1
81 * Validated XHTML (Wolfgang Borgert).
82 * Added description of test classes and test cases.
83
84 Version in 0.8.0
85 * Define Template_mixin class for customization.
86 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
87
88 Version in 0.7.1
89 * Back port to Python 2.3 (Frank Horowitz).
90 * Fix missing scroll bars in detail log (Podi).
91 """
92
93 # TODO: color stderr
94 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
95
96 import datetime
97 import io
98 import sys
99 import time
100 import unittest
101 from xml.sax import saxutils
102
103
104 # ------------------------------------------------------------------------
105 # The redirectors below are used to capture output during testing. Output
106 # sent to sys.stdout and sys.stderr are automatically captured. However
107 # in some cases sys.stdout is already cached before HTMLTestRunner is
108 # invoked (e.g. calling logging.basicConfig). In order to capture those
109 # output, use the redirectors for the cached stream.
110 #
111 # e.g.
112 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
113 # >>>
114
115 def to_unicode(s):
116 try:
117 return str(s)
118 except UnicodeDecodeError:
119 # s is non ascii byte string
120 return s.decode('unicode_escape')
121
122 class OutputRedirector(object):
123 """ Wrapper to redirect stdout or stderr """
124 def __init__(self, fp):
125 self.fp = fp
126
127 def write(self, s):
128 self.fp.write(to_unicode(s))
129
130 def writelines(self, lines):
131 lines = map(to_unicode, lines)
132 self.fp.writelines(lines)
133
134 def flush(self):
135 self.fp.flush()
136
137 stdout_redirector = OutputRedirector(sys.stdout)
138 stderr_redirector = OutputRedirector(sys.stderr)
139
140
141
142 # ----------------------------------------------------------------------
143 # Template
144
145 class Template_mixin(object):
146 """
147 Define a HTML template for report customerization and generation.
148
149 Overall structure of an HTML report
150
151 HTML
152 +------------------------+
153 |<html> |
154 | <head> |
155 | |
156 | STYLESHEET |
157 | +----------------+ |
158 | | | |
159 | +----------------+ |
160 | |
161 | </head> |
162 | |
163 | <body> |
164 | |
165 | HEADING |
166 | +----------------+ |
167 | | | |
168 | +----------------+ |
169 | |
170 | REPORT |
171 | +----------------+ |
172 | | | |
173 | +----------------+ |
174 | |
175 | ENDING |
176 | +----------------+ |
177 | | | |
178 | +----------------+ |
179 | |
180 | </body> |
181 |</html> |
182 +------------------------+
183 """
184
185 STATUS = {
186 0: 'pass',
187 1: 'fail',
188 2: 'error',
189 }
190
191 DEFAULT_TITLE = 'Unit Test Report'
192 DEFAULT_DESCRIPTION = ''
193
194 # ------------------------------------------------------------------------
195 # HTML Template
196
197 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
198 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
199 <html xmlns="http://www.w3.org/1999/xhtml">
200 <head>
201 <title>%(title)s</title>
202 <meta name="generator" content="%(generator)s"/>
203 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
204 %(stylesheet)s
205 </head>
206 <body>
207 <script language="javascript" type="text/javascript"><!--
208 output_list = Array();
209
210 /* level - 0:Summary; 1:Failed; 2:All */
211 function showCase(level) {
212 trs = document.getElementsByTagName("tr");
213 for (var i = 0; i < trs.length; i++) {
214 tr = trs[i];
215 id = tr.id;
216 if (id.substr(0,2) == 'ft') {
217 if (level < 1) {
218 tr.className = 'hiddenRow';
219 }
220 else {
221 tr.className = '';
222 }
223 }
224 if (id.substr(0,2) == 'pt') {
225 if (level > 1) {
226 tr.className = '';
227 }
228 else {
229 tr.className = 'hiddenRow';
230 }
231 }
232 }
233 }
234
235
236 function showClassDetail(cid, count) {
237 var id_list = Array(count);
238 var toHide = 1;
239 for (var i = 0; i < count; i++) {
240 tid0 = 't' + cid.substr(1) + '.' + (i+1);
241 tid = 'f' + tid0;
242 tr = document.getElementById(tid);
243 if (!tr) {
244 tid = 'p' + tid0;
245 tr = document.getElementById(tid);
246 }
247 id_list[i] = tid;
248 if (tr.className) {
249 toHide = 0;
250 }
251 }
252 for (var i = 0; i < count; i++) {
253 tid = id_list[i];
254 if (toHide) {
255 document.getElementById('div_'+tid).style.display = 'none'
256 document.getElementById(tid).className = 'hiddenRow';
257 }
258 else {
259 document.getElementById(tid).className = '';
260 }
261 }
262 }
263
264
265 function showTestDetail(div_id){
266 var details_div = document.getElementById(div_id)
267 var displayState = details_div.style.display
268 // alert(displayState)
269 if (displayState != 'block' ) {
270 displayState = 'block'
271 details_div.style.display = 'block'
272 }
273 else {
274 details_div.style.display = 'none'
275 }
276 }
277
278
279 function html_escape(s) {
280 s = s.replace(/&/g,'&');
281 s = s.replace(/</g,'<');
282 s = s.replace(/>/g,'>');
283 return s;
284 }
285
286 /* obsoleted by detail in <div>
287 function showOutput(id, name) {
288 var w = window.open("", //url
289 name,
290 "resizable,scrollbars,status,width=800,height=450");
291 d = w.document;
292 d.write("<pre>");
293 d.write(html_escape(output_list[id]));
294 d.write("\n");
295 d.write("<a href='javascript:window.close()'>close</a>\n");
296 d.write("</pre>\n");
297 d.close();
298 }
299 */
300 --></script>
301
302 %(heading)s
303 %(report)s
304 %(ending)s
305
306 </body>
307 </html>
308 """
309 # variables: (title, generator, stylesheet, heading, report, ending)
310
311
312 # ------------------------------------------------------------------------
313 # Stylesheet
314 #
315 # alternatively use a <link> for external style sheet, e.g.
316 # <link rel="stylesheet" href="$url" type="text/css">
317
318 STYLESHEET_TMPL = """
319 <style type="text/css" media="screen">
320 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
321 table { font-size: 100%; }
322 pre { }
323
324 /* -- heading ---------------------------------------------------------------------- */
325 h1 {
326 font-size: 16pt;
327 color: gray;
328 }
329 .heading {
330 margin-top: 0ex;
331 margin-bottom: 1ex;
332 }
333
334 .heading .attribute {
335 margin-top: 1ex;
336 margin-bottom: 0;
337 }
338
339 .heading .description {
340 margin-top: 4ex;
341 margin-bottom: 6ex;
342 }
343
344 /* -- css div popup ------------------------------------------------------------------------ */
345 a.popup_link {
346 }
347
348 a.popup_link:hover {
349 color: red;
350 }
351
352 .popup_window {
353 display: none;
354 position: relative;
355 left: 0px;
356 top: 0px;
357 /*border: solid #627173 1px; */
358 padding: 10px;
359 background-color: #E6E6D6;
360 font-family: "Lucida Console", "Courier New", Courier, monospace;
361 text-align: left;
362 font-size: 8pt;
363 width: 500px;
364 }
365
366 }
367 /* -- report ------------------------------------------------------------------------ */
368 #show_detail_line {
369 margin-top: 3ex;
370 margin-bottom: 1ex;
371 }
372 #result_table {
373 width: 80%;
374 border-collapse: collapse;
375 border: 1px solid #777;
376 }
377 #header_row {
378 font-weight: bold;
379 color: white;
380 background-color: #777;
381 }
382 #result_table td {
383 border: 1px solid #777;
384 padding: 2px;
385 }
386 #total_row { font-weight: bold; }
387 .passClass { background-color: #6c6; }
388 .failClass { background-color: #c60; }
389 .errorClass { background-color: #c00; }
390 .passCase { color: #6c6; }
391 .failCase { color: #c60; font-weight: bold; }
392 .errorCase { color: #c00; font-weight: bold; }
393 .hiddenRow { display: none; }
394 .testcase { margin-left: 2em; }
395
396
397 /* -- ending ---------------------------------------------------------------------- */
398 #ending {
399 }
400
401 </style>
402 """
403
404
405
406 # ------------------------------------------------------------------------
407 # Heading
408 #
409
410 HEADING_TMPL = """<div class='heading'>
411 <h1>%(title)s</h1>
412 %(parameters)s
413 <p class='description'>%(description)s</p>
414 </div>
415
416 """ # variables: (title, parameters, description)
417
418 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
419 """ # variables: (name, value)
420
421
422
423 # ------------------------------------------------------------------------
424 # Report
425 #
426
427 REPORT_TMPL = """
428 <p id='show_detail_line'>Show
429 <a href='javascript:showCase(0)'>Summary</a>
430 <a href='javascript:showCase(1)'>Failed</a>
431 <a href='javascript:showCase(2)'>All</a>
432 </p>
433 <table id='result_table'>
434 <colgroup>
435 <col align='left' />
436 <col align='right' />
437 <col align='right' />
438 <col align='right' />
439 <col align='right' />
440 <col align='right' />
441 </colgroup>
442 <tr id='header_row'>
443 <td>Test Group/Test case</td>
444 <td>Count</td>
445 <td>Pass</td>
446 <td>Fail</td>
447 <td>Error</td>
448 <td>View</td>
449 </tr>
450 %(test_list)s
451 <tr id='total_row'>
452 <td>Total</td>
453 <td>%(count)s</td>
454 <td>%(Pass)s</td>
455 <td>%(fail)s</td>
456 <td>%(error)s</td>
457 <td> </td>
458 </tr>
459 </table>
460 """ # variables: (test_list, count, Pass, fail, error)
461
462 REPORT_CLASS_TMPL = r"""
463 <tr class='%(style)s'>
464 <td>%(desc)s</td>
465 <td>%(count)s</td>
466 <td>%(Pass)s</td>
467 <td>%(fail)s</td>
468 <td>%(error)s</td>
469 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
470 </tr>
471 """ # variables: (style, desc, count, Pass, fail, error, cid)
472
473
474 REPORT_TEST_WITH_OUTPUT_TMPL = r"""
475 <tr id='%(tid)s' class='%(Class)s'>
476 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
477 <td colspan='5' align='center'>
478
479 <!--css div popup start-->
480 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
481 %(status)s</a>
482
483 <div id='div_%(tid)s' class="popup_window">
484 <div style='text-align: right; color:red;cursor:pointer'>
485 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
486 [x]</a>
487 </div>
488 <pre>
489 %(script)s
490 </pre>
491 </div>
492 <!--css div popup end-->
493
494 </td>
495 </tr>
496 """ # variables: (tid, Class, style, desc, status)
497
498
499 REPORT_TEST_NO_OUTPUT_TMPL = r"""
500 <tr id='%(tid)s' class='%(Class)s'>
501 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
502 <td colspan='5' align='center'>%(status)s</td>
503 </tr>
504 """ # variables: (tid, Class, style, desc, status)
505
506
507 REPORT_TEST_OUTPUT_TMPL = r"""
508 %(id)s: %(output)s
509 """ # variables: (id, output)
510
511
512
513 # ------------------------------------------------------------------------
514 # ENDING
515 #
516
517 ENDING_TMPL = """<div id='ending'> </div>"""
518
519 # -------------------- The end of the Template class -------------------
520
521
522 TestResult = unittest.TestResult
523
524 class _TestResult(TestResult):
525 # note: _TestResult is a pure representation of results.
526 # It lacks the output and reporting ability compares to unittest._TextTestResult.
527
528 def __init__(self, verbosity=1):
529 TestResult.__init__(self)
530 self.outputBuffer = io.StringIO()
531 self.stdout0 = None
532 self.stderr0 = None
533 self.success_count = 0
534 self.failure_count = 0
535 self.error_count = 0
536 self.verbosity = verbosity
537
538 # result is a list of result in 4 tuple
539 # (
540 # result code (0: success; 1: fail; 2: error),
541 # TestCase object,
542 # Test output (byte string),
543 # stack trace,
544 # )
545 self.result = []
546
547
548 def startTest(self, test):
549 TestResult.startTest(self, test)
550 # just one buffer for both stdout and stderr
551 stdout_redirector.fp = self.outputBuffer
552 stderr_redirector.fp = self.outputBuffer
553 self.stdout0 = sys.stdout
554 self.stderr0 = sys.stderr
555 sys.stdout = stdout_redirector
556 sys.stderr = stderr_redirector
557
558
559 def complete_output(self):
560 """
561 Disconnect output redirection and return buffer.
562 Safe to call multiple times.
563 """
564 if self.stdout0:
565 sys.stdout = self.stdout0
566 sys.stderr = self.stderr0
567 self.stdout0 = None
568 self.stderr0 = None
569 return self.outputBuffer.getvalue()
570
571
572 def stopTest(self, test):
573 # Usually one of addSuccess, addError or addFailure would have been called.
574 # But there are some path in unittest that would bypass this.
575 # We must disconnect stdout in stopTest(), which is guaranteed to be called.
576 self.complete_output()
577
578
579 def addSuccess(self, test):
580 self.success_count += 1
581 TestResult.addSuccess(self, test)
582 output = self.complete_output()
583 self.result.append((0, test, output, ''))
584 if self.verbosity > 1:
585 sys.stderr.write('ok ')
586 sys.stderr.write(str(test))
587 sys.stderr.write('\n')
588 else:
589 sys.stderr.write('.')
590
591 def addError(self, test, err):
592 self.error_count += 1
593 TestResult.addError(self, test, err)
594 _, _exc_str = self.errors[-1]
595 output = self.complete_output()
596 self.result.append((2, test, output, _exc_str))
597 if self.verbosity > 1:
598 sys.stderr.write('E ')
599 sys.stderr.write(str(test))
600 sys.stderr.write('\n')
601 else:
602 sys.stderr.write('E')
603
604 def addFailure(self, test, err):
605 self.failure_count += 1
606 TestResult.addFailure(self, test, err)
607 _, _exc_str = self.failures[-1]
608 output = self.complete_output()
609 self.result.append((1, test, output, _exc_str))
610 if self.verbosity > 1:
611 sys.stderr.write('F ')
612 sys.stderr.write(str(test))
613 sys.stderr.write('\n')
614 else:
615 sys.stderr.write('F')
616
617
618 class HTMLTestRunner(Template_mixin):
619 """
620 """
621 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
622 self.stream = stream
623 self.verbosity = verbosity
624 if title is None:
625 self.title = self.DEFAULT_TITLE
626 else:
627 self.title = title
628 if description is None:
629 self.description = self.DEFAULT_DESCRIPTION
630 else:
631 self.description = description
632
633 self.startTime = datetime.datetime.now()
634
635
636 def run(self, test):
637 "Run the given test case or test suite."
638 result = _TestResult(self.verbosity)
639 test(result)
640 self.stopTime = datetime.datetime.now()
641 self.generateReport(test, result)
642 print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime - self.startTime))
643 # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
644 return result
645
646
647 def sortResult(self, result_list):
648 # unittest does not seems to run in any particular order.
649 # Here at least we want to group them together by class.
650 rmap = {}
651 classes = []
652 for n,t,o,e in result_list:
653 cls = t.__class__
654 if not cls in rmap:
655 rmap[cls] = []
656 classes.append(cls)
657 rmap[cls].append((n,t,o,e))
658 r = [(cls, rmap[cls]) for cls in classes]
659 return r
660
661
662 def getReportAttributes(self, result):
663 """
664 Return report attributes as a list of (name, value).
665 Override this to add custom attributes.
666 """
667 startTime = str(self.startTime)[:19]
668 duration = str(self.stopTime - self.startTime)
669 status = []
670 if result.success_count: status.append('Pass %s' % result.success_count)
671 if result.failure_count: status.append('Failure %s' % result.failure_count)
672 if result.error_count: status.append('Error %s' % result.error_count )
673 if status:
674 status = ' '.join(status)
675 else:
676 status = 'none'
677 return [
678 ('Start Time', startTime),
679 ('Duration', duration),
680 ('Status', status),
681 ]
682
683
684 def generateReport(self, test, result):
685 report_attrs = self.getReportAttributes(result)
686 generator = 'HTMLTestRunner %s' % __version__
687 stylesheet = self._generate_stylesheet()
688 heading = self._generate_heading(report_attrs)
689 report = self._generate_report(result)
690 ending = self._generate_ending()
691 output = self.HTML_TMPL % dict(
692 title = saxutils.escape(self.title),
693 generator = generator,
694 stylesheet = stylesheet,
695 heading = heading,
696 report = report,
697 ending = ending,
698 )
699 self.stream.write(output.encode('utf8'))
700
701
702 def _generate_stylesheet(self):
703 return self.STYLESHEET_TMPL
704
705
706 def _generate_heading(self, report_attrs):
707 a_lines = []
708 for name, value in report_attrs:
709 line = self.HEADING_ATTRIBUTE_TMPL % dict(
710 name = saxutils.escape(name),
711 value = saxutils.escape(value),
712 )
713 a_lines.append(line)
714 heading = self.HEADING_TMPL % dict(
715 title = saxutils.escape(self.title),
716 parameters = ''.join(a_lines),
717 description = saxutils.escape(self.description),
718 )
719 return heading
720
721
722 def _generate_report(self, result):
723 rows = []
724 sortedResult = self.sortResult(result.result)
725 for cid, (cls, cls_results) in enumerate(sortedResult):
726 # subtotal for a class
727 np = nf = ne = 0
728 for n,t,o,e in cls_results:
729 if n == 0: np += 1
730 elif n == 1: nf += 1
731 else: ne += 1
732
733 # format class description
734 if cls.__module__ == "__main__":
735 name = cls.__name__
736 else:
737 name = "%s.%s" % (cls.__module__, cls.__name__)
738 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
739 desc = doc and '%s: %s' % (name, doc) or name
740
741 row = self.REPORT_CLASS_TMPL % dict(
742 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
743 desc = desc,
744 count = np+nf+ne,
745 Pass = np,
746 fail = nf,
747 error = ne,
748 cid = 'c%s' % (cid+1),
749 )
750 rows.append(row)
751
752 for tid, (n,t,o,e) in enumerate(cls_results):
753 self._generate_report_test(rows, cid, tid, n, t, o, e)
754
755 report = self.REPORT_TMPL % dict(
756 test_list = ''.join(rows),
757 count = str(result.success_count+result.failure_count+result.error_count),
758 Pass = str(result.success_count),
759 fail = str(result.failure_count),
760 error = str(result.error_count),
761 )
762 return report
763
764
765 def _generate_report_test(self, rows, cid, tid, n, t, o, e):
766 # e.g. 'pt1.1', 'ft1.1', etc
767 has_output = bool(o or e)
768 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
769 name = t.id().split('.')[-1]
770 doc = t.shortDescription() or ""
771 desc = doc and ('%s: %s' % (name, doc)) or name
772 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
773
774 # o and e should be byte string because they are collected from stdout and stderr?
775 if isinstance(o,str):
776 # TODO: some problem with 'string_escape': it escape \n and mess up formating
777 # uo = unicode(o.encode('string_escape'))
778 uo = e
779 else:
780 uo = o
781 if isinstance(e,str):
782 # TODO: some problem with 'string_escape': it escape \n and mess up formating
783 # ue = unicode(e.encode('string_escape'))
784 ue = e
785 else:
786 ue = e
787
788 script = self.REPORT_TEST_OUTPUT_TMPL % dict(
789 id = tid,
790 output = saxutils.escape(uo+ue),
791 )
792
793 row = tmpl % dict(
794 tid = tid,
795 Class = (n == 0 and 'hiddenRow' or 'none'),
796 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
797 desc = desc,
798 script = script,
799 status = self.STATUS[n],
800 )
801 rows.append(row)
802 if not has_output:
803 return
804
805 def _generate_ending(self):
806 return self.ENDING_TMPL
807
808
809 ##############################################################################
810 # Facilities for running tests from the command line
811 ##############################################################################
812
813 # Note: Reuse unittest.TestProgram to launch test. In the future we may
814 # build our own launcher to support more specific command line
815 # parameters like test title, CSS, etc.
816 class TestProgram(unittest.TestProgram):
817 """
818 A variation of the unittest.TestProgram. Please refer to the base
819 class for command line parameters.
820 """
821 def runTests(self):
822 # Pick HTMLTestRunner as the default test runner.
823 # base class's testRunner parameter is not useful because it means
824 # we have to instantiate HTMLTestRunner before we know self.verbosity.
825 if self.testRunner is None:
826 self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
827 unittest.TestProgram.runTests(self)
828
829 main = TestProgram
830
831 ##############################################################################
832 # Executing this module from the command line
833 ##############################################################################
834
835 if __name__ == "__main__":
836 main(module=None)
2. 将HTMLTestRunner.py 放到 "
/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5
"(以mac为例)
3.主运行文件
1 # -*- coding:utf-8 -*-
2 import unittest
3 import os
4 import time
5 import HTMLTestRunner
6
7
8 def allTests():
9 path = os.path.dirname(__file__)
10 print(path)
11 suit = unittest.defaultTestLoader.discover(path,pattern='test2.py')
12 return suit
13
14 def getNowTime():
15 return time.strftime('%Y-%m-%d %H_%M_%S')
16
17 def run():
18 fileName = os.path.join(os.path.dirname(__file__),getNowTime()+'report.html')
19 fp = open(fileName,'wb')
20 runner = HTMLTestRunner.HTMLTestRunner(stream=fp,title='UI 自动化测试报告',description="详情")
21 runner.run(allTests())
22
23 if __name__ == '__main__':
24 run()
4. report

