About
简单使用

import webbrowser
import unittest
import HTMLTestRunner
import BSTestRunner
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('Foo'.isupper())
if __name__ == '__main__':
suite = unittest.makeSuite(TestStringMethods)
f1 = open('result1.html', 'wb')
f2 = open('result2.html', 'wb')
HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(
suite)
suite = unittest.makeSuite(TestStringMethods)
BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite)
f1.close()
f2.close()
webbrowser.open('result1.html')
webbrowser.open('result2.html')
-
stream是文件句柄。
-
title是测试报告的title。
-
description是测试报告的描述信息。
这样在本地就生成了result1.html和result2.html两个HTML文件:


OK,还是比较完美的,再来一点优化:
优化版
优化其实很简单:

import webbrowser
import unittest
import HTMLTestRunner
import BSTestRunner
class TestStringMethods(unittest.TestCase):
def test_upper(self):
"""判断 foo.upper() 是否等于 FOO"""
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
""" 判断 Foo 是否为大写形式 """
self.assertTrue('Foo'.isupper())
if __name__ == '__main__':
suite = unittest.makeSuite(TestStringMethods)
f1 = open('result1.html', 'wb')
f2 = open('result2.html', 'wb')
HTMLTestRunner.HTMLTestRunner(stream=f1, title='HTMLTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(
suite)
suite = unittest.makeSuite(TestStringMethods)
BSTestRunner.BSTestRunner(stream=f2, title='BSTestRunner版本关于upper的测试报告', description='判断upper的测试用例执行情况').run(suite)
f1.close()
f2.close()
webbrowser.open('result1.html')
webbrowser.open('result2.html')
其实就是为每个用例方法添加上注释说明。


Python2.x版本

import webbrowser
import unittest
import HTMLTestRunner
import BSTestRunner
class TestStringMethods(unittest.TestCase):
def test_upper(self):
u"""判断 foo.upper() 是否等于 FOO"""
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
u""" 判断 Foo 是否为大写形式 """
self.assertTrue('Foo'.isupper())
if __name__ == '__main__':
suite = unittest.makeSuite(TestStringMethods)
f1 = open('result1.html', 'wb')
f2 = open('result2.html', 'wb')
HTMLTestRunner.HTMLTestRunner(
stream=f1,
title=u'HTMLTestRunner版本关于upper的测试报告',
description=u'判断upper的测试用例执行情况').run(suite)
suite = unittest.makeSuite(TestStringMethods)
BSTestRunner.BSTestRunner(
stream=f2,
title=u'BSTestRunner版本关于upper的测试报告',
description=u'判断upper的测试用例执行情况').run(suite)
f1.close()
f2.close()
webbrowser.open('result1.html')
webbrowser.open('result2.html')
各版本的两文件的源码,保存到指定位置即可。

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,'&');
270 s = s.replace(/</g,'<');
271 s = s.replace(/>/g,'>');
272 return s;
273 }
274
275 /* obsoleted by detail in <div>
276 function showOutput(id, name) {
277 var w = window.open("", //url
278 name,
279 "resizable,scrollbars,status,width=800,height=450");
280 d = w.document;
281 d.write("<pre>");
282 d.write(html_escape(output_list[id]));
283 d.write("\n");
284 d.write("<a href='javascript:window.close()'>close</a>\n");
285 d.write("</pre>\n");
286 d.close();
287 }
288 */
289 --></script>
290
291 %(heading)s
292 %(report)s
293 %(ending)s
294
295 </body>
296 </html>
297 """
298 # variables: (title, generator, stylesheet, heading, report, ending)
299
300
301 # ------------------------------------------------------------------------
302 # Stylesheet
303 #
304 # alternatively use a <link> for external style sheet, e.g.
305 # <link rel="stylesheet" href="$url" type="text/css">
306
307 STYLESHEET_TMPL = """
308 <style type="text/css" media="screen">
309 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
310 table { font-size: 100%; }
311 pre { }
312
313 /* -- heading ---------------------------------------------------------------------- */
314 h1 {
315 font-size: 16pt;
316 color: gray;
317 }
318 .heading {
319 margin-top: 0ex;
320 margin-bottom: 1ex;
321 }
322
323 .heading .attribute {
324 margin-top: 1ex;
325 margin-bottom: 0;
326 }
327
328 .heading .description {
329 margin-top: 4ex;
330 margin-bottom: 6ex;
331 }
332
333 /* -- css div popup ------------------------------------------------------------------------ */
334 a.popup_link {
335 }
336
337 a.popup_link:hover {
338 color: red;
339 }
340
341 .popup_window {
342 display: none;
343 position: relative;
344 left: 0px;
345 top: 0px;
346 /*border: solid #627173 1px; */
347 padding: 10px;
348 background-color: #E6E6D6;
349 font-family: "Lucida Console", "Courier New", Courier, monospace;
350 text-align: left;
351 font-size: 8pt;
352 width: 500px;
353 }
354
355 }
356 /* -- report ------------------------------------------------------------------------ */
357 #show_detail_line {
358 margin-top: 3ex;
359 margin-bottom: 1ex;
360 }
361 #result_table {
362 width: 80%;
363 border-collapse: collapse;
364 border: 1px solid #777;
365 }
366 #header_row {
367 font-weight: bold;
368 color: white;
369 background-color: #777;
370 }
371 #result_table td {
372 border: 1px solid #777;
373 padding: 2px;
374 }
375 #total_row { font-weight: bold; }
376 .passClass { background-color: #6c6; }
377 .failClass { background-color: #c60; }
378 .errorClass { background-color: #c00; }
379 .passCase { color: #6c6; }
380 .failCase { color: #c60; font-weight: bold; }
381 .errorCase { color: #c00; font-weight: bold; }
382 .hiddenRow { display: none; }
383 .testcase { margin-left: 2em; }
384
385
386 /* -- ending ---------------------------------------------------------------------- */
387 #ending {
388 }
389
390 </style>
391 """
392
393
394
395 # ------------------------------------------------------------------------
396 # Heading
397 #
398
399 HEADING_TMPL = """<div class='heading'>
400 <h1>%(title)s</h1>
401 %(parameters)s
402 <p class='description'>%(description)s</p>
403 </div>
404
405 """ # variables: (title, parameters, description)
406
407 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
408 """ # variables: (name, value)
409
410
411
412 # ------------------------------------------------------------------------
413 # Report
414 #
415
416 REPORT_TMPL = """
417 <p id='show_detail_line'>Show
418 <a href='javascript:showCase(0)'>Summary</a>
419 <a href='javascript:showCase(1)'>Failed</a>
420 <a href='javascript:showCase(2)'>All</a>
421 </p>
422 <table id='result_table'>
423 <colgroup>
424 <col align='left' />
425 <col align='right' />
426 <col align='right' />
427 <col align='right' />
428 <col align='right' />
429 <col align='right' />
430 </colgroup>
431 <tr id='header_row'>
432 <td>Test Group/Test case</td>
433 <td>Count</td>
434 <td>Pass</td>
435 <td>Fail</td>
436 <td>Error</td>
437 <td>View</td>
438 </tr>
439 %(test_list)s
440 <tr id='total_row'>
441 <td>Total</td>
442 <td>%(count)s</td>
443 <td>%(Pass)s</td>
444 <td>%(fail)s</td>
445 <td>%(error)s</td>
446 <td> </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'> </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.BytesIO()
540 stdout_redirector.fp = self.outputBuffer
541 stderr_redirector.fp = self.outputBuffer
542 self.stdout0 = sys.stdout
543 self.stderr0 = sys.stderr
544 sys.stdout = stdout_redirector
545 sys.stderr = stderr_redirector
546
547
548 def complete_output(self):
549 """
550 Disconnect output redirection and return buffer.
551 Safe to call multiple times.
552 """
553 if self.stdout0:
554 sys.stdout = self.stdout0
555 sys.stderr = self.stderr0
556 self.stdout0 = None
557 self.stderr0 = None
558 return self.outputBuffer.getvalue()
559
560
561 def stopTest(self, test):
562 # Usually one of addSuccess, addError or addFailure would have been called.
563 # But there are some path in unittest that would bypass this.
564 # We must disconnect stdout in stopTest(), which is guaranteed to be called.
565 self.complete_output()
566
567
568 def addSuccess(self, test):
569 self.success_count += 1
570 TestResult.addSuccess(self, test)
571 output = self.complete_output()
572 self.result.append((0, test, output, ''))
573 if self.verbosity > 1:
574 sys.stderr.write('ok ')
575 sys.stderr.write(str(test))
576 sys.stderr.write('\n')
577 else:
578 sys.stderr.write('.')
579
580 def addError(self, test, err):
581 self.error_count += 1
582 TestResult.addError(self, test, err)
583 _, _exc_str = self.errors[-1]
584 output = self.complete_output()
585 self.result.append((2, test, output, _exc_str))
586 if self.verbosity > 1:
587 sys.stderr.write('E ')
588 sys.stderr.write(str(test))
589 sys.stderr.write('\n')
590 else:
591 sys.stderr.write('E')
592
593 def addFailure(self, test, err):
594 self.failure_count += 1
595 TestResult.addFailure(self, test, err)
596 _, _exc_str = self.failures[-1]
597 output = self.complete_output()
598 self.result.append((1, test, output, _exc_str))
599 if self.verbosity > 1:
600 sys.stderr.write('F ')
601 sys.stderr.write(str(test))
602 sys.stderr.write('\n')
603 else:
604 sys.stderr.write('F')
605
606
607 class HTMLTestRunner(Template_mixin):
608 """
609 """
610 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
611 self.stream = stream
612 self.verbosity = verbosity
613 if title is None:
614 self.title = self.DEFAULT_TITLE
615 else:
616 self.title = title
617 if description is None:
618 self.description = self.DEFAULT_DESCRIPTION
619 else:
620 self.description = description
621
622 self.startTime = datetime.datetime.now()
623
624
625 def run(self, test):
626 "Run the given test case or test suite."
627 result = _TestResult(self.verbosity)
628 test(result)
629 self.stopTime = datetime.datetime.now()
630 self.generateReport(test, result)
631 print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))
632 return result
633
634
635 def sortResult(self, result_list):
636 # unittest does not seems to run in any particular order.
637 # Here at least we want to group them together by class.
638 rmap = {}
639 classes = []
640 for n,t,o,e in result_list:
641 cls = t.__class__
642 if not cls in rmap:
643 rmap[cls] = []
644 classes.append(cls)
645 rmap[cls].append((n,t,o,e))
646 r = [(cls, rmap[cls]) for cls in classes]
647 return r
648
649
650 def getReportAttributes(self, result):
651 """
652 Return report attributes as a list of (name, value).
653 Override this to add custom attributes.
654 """
655 startTime = str(self.startTime)[:19]
656 duration = str(self.stopTime - self.startTime)
657 status = []
658 if result.success_count: status.append('Pass %s' % result.success_count)
659 if result.failure_count: status.append('Failure %s' % result.failure_count)
660 if result.error_count: status.append('Error %s' % result.error_count )
661 if status:
662 status = ' '.join(status)
663 else:
664 status = 'none'
665 return [
666 ('Start Time', startTime),
667 ('Duration', duration),
668 ('Status', status),
669 ]
670
671
672 def generateReport(self, test, result):
673 report_attrs = self.getReportAttributes(result)
674 generator = 'HTMLTestRunner %s' % __version__
675 stylesheet = self._generate_stylesheet()
676 heading = self._generate_heading(report_attrs)
677 report = self._generate_report(result)
678 ending = self._generate_ending()
679 output = self.HTML_TMPL % dict(
680 title = saxutils.escape(self.title),
681 generator = generator,
682 stylesheet = stylesheet,
683 heading = heading,
684 report = report,
685 ending = ending,
686 )
687 self.stream.write(output.encode('utf8'))
688
689
690 def _generate_stylesheet(self):
691 return self.STYLESHEET_TMPL
692
693
694 def _generate_heading(self, report_attrs):
695 a_lines = []
696 for name, value in report_attrs:
697 line = self.HEADING_ATTRIBUTE_TMPL % dict(
698 name = saxutils.escape(name),
699 value = saxutils.escape(value),
700 )
701 a_lines.append(line)
702 heading = self.HEADING_TMPL % dict(
703 title = saxutils.escape(self.title),
704 parameters = ''.join(a_lines),
705 description = saxutils.escape(self.description),
706 )
707 return heading
708
709
710 def _generate_report(self, result):
711 rows = []
712 sortedResult = self.sortResult(result.result)
713 for cid, (cls, cls_results) in enumerate(sortedResult):
714 # subtotal for a class
715 np = nf = ne = 0
716 for n,t,o,e in cls_results:
717 if n == 0: np += 1
718 elif n == 1: nf += 1
719 else: ne += 1
720
721 # format class description
722 if cls.__module__ == "__main__":
723 name = cls.__name__
724 else:
725 name = "%s.%s" % (cls.__module__, cls.__name__)
726 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
727 desc = doc and '%s: %s' % (name, doc) or name
728
729 row = self.REPORT_CLASS_TMPL % dict(
730 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
731 desc = desc,
732 count = np+nf+ne,
733 Pass = np,
734 fail = nf,
735 error = ne,
736 cid = 'c%s' % (cid+1),
737 )
738 rows.append(row)
739
740 for tid, (n,t,o,e) in enumerate(cls_results):
741 self._generate_report_test(rows, cid, tid, n, t, o, e)
742
743 report = self.REPORT_TMPL % dict(
744 test_list = ''.join(rows),
745 count = str(result.success_count+result.failure_count+result.error_count),
746 Pass = str(result.success_count),
747 fail = str(result.failure_count),
748 error = str(result.error_count),
749 )
750 return report
751
752
753 def _generate_report_test(self, rows, cid, tid, n, t, o, e):
754 # e.g. 'pt1.1', 'ft1.1', etc
755 has_output = bool(o or e)
756 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
757 name = t.id().split('.')[-1]
758 doc = t.shortDescription() or ""
759 desc = doc and ('%s: %s' % (name, doc)) or name
760 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
761
762 # o and e should be byte string because they are collected from stdout and stderr?
763 if isinstance(o,str):
764 # TODO: some problem with 'string_escape': it escape \n and mess up formating
765 # uo = unicode(o.encode('string_escape'))
766 uo = o.decode('latin-1')
767 else:
768 uo = o
769 if isinstance(e,str):
770 # TODO: some problem with 'string_escape': it escape \n and mess up formating
771 # ue = unicode(e.encode('string_escape'))
772 ue = e
773 else:
774 ue = e
775
776 script = self.REPORT_TEST_OUTPUT_TMPL % dict(
777 id = tid,
778 output = saxutils.escape(str(uo)+ue),
779 )
780
781 row = tmpl % dict(
782 tid = tid,
783 Class = (n == 0 and 'hiddenRow' or 'none'),
784 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
785 desc = desc,
786 script = script,
787 status = self.STATUS[n],
788 )
789 rows.append(row)
790 if not has_output:
791 return
792
793 def _generate_ending(self):
794 return self.ENDING_TMPL
795
796
797 ##############################################################################
798 # Facilities for running tests from the command line
799 ##############################################################################
800
801 # Note: Reuse unittest.TestProgram to launch test. In the future we may
802 # build our own launcher to support more specific command line
803 # parameters like test title, CSS, etc.
804 class TestProgram(unittest.TestProgram):
805 """
806 A variation of the unittest.TestProgram. Please refer to the base
807 class for command line parameters.
808 """
809 def runTests(self):
810 # Pick HTMLTestRunner as the default test runner.
811 # base class's testRunner parameter is not useful because it means
812 # we have to instantiate HTMLTestRunner before we know self.verbosity.
813 if self.testRunner is None:
814 self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
815 unittest.TestProgram.runTests(self)
816
817 main = TestProgram
818
819 ##############################################################################
820 # Executing this module from the command line
821 ##############################################################################
822
823 if __name__ == "__main__":
824 main(module=None)

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

1 """
2 A TestRunner for use with the Python unit testing framework. It
3 generates a HTML report to show the result at a glance.
4
5 The simplest way to use this is to invoke its main method. E.g.
6
7 import unittest
8 import HTMLTestRunner
9
10 ... define your tests ...
11
12 if __name__ == '__main__':
13 HTMLTestRunner.main()
14
15
16 For more customization options, instantiates a HTMLTestRunner object.
17 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
18
19 # output to a file
20 fp = file('my_report.html', 'wb')
21 runner = HTMLTestRunner.HTMLTestRunner(
22 stream=fp,
23 title='My unit test',
24 description='This demonstrates the report output by HTMLTestRunner.'
25 )
26
27 # Use an external stylesheet.
28 # See the Template_mixin class for more customizable options
29 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
30
31 # run the test
32 runner.run(my_test_suite)
33
34
35 ------------------------------------------------------------------------
36 Copyright (c) 2004-2007, Wai Yip Tung
37 All rights reserved.
38
39 Redistribution and use in source and binary forms, with or without
40 modification, are permitted provided that the following conditions are
41 met:
42
43 * Redistributions of source code must retain the above copyright notice,
44 this list of conditions and the following disclaimer.
45 * Redistributions in binary form must reproduce the above copyright
46 notice, this list of conditions and the following disclaimer in the
47 documentation and/or other materials provided with the distribution.
48 * Neither the name Wai Yip Tung nor the names of its contributors may be
49 used to endorse or promote products derived from this software without
50 specific prior written permission.
51
52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
63 """
64
65 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
66
67 __author__ = "Wai Yip Tung"
68 __version__ = "0.8.2"
69
70
71 """
72 Change History
73
74 Version 0.8.2
75 * Show output inline instead of popup window (Viorel Lupu).
76
77 Version in 0.8.1
78 * Validated XHTML (Wolfgang Borgert).
79 * Added description of test classes and test cases.
80
81 Version in 0.8.0
82 * Define Template_mixin class for customization.
83 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
84
85 Version in 0.7.1
86 * Back port to Python 2.3 (Frank Horowitz).
87 * Fix missing scroll bars in detail log (Podi).
88 """
89
90 # TODO: color stderr
91 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
92
93 import datetime
94 import StringIO
95 import sys
96 import time
97 import unittest
98 from xml.sax import saxutils
99
100
101 # ------------------------------------------------------------------------
102 # The redirectors below are used to capture output during testing. Output
103 # sent to sys.stdout and sys.stderr are automatically captured. However
104 # in some cases sys.stdout is already cached before HTMLTestRunner is
105 # invoked (e.g. calling logging.basicConfig). In order to capture those
106 # output, use the redirectors for the cached stream.
107 #
108 # e.g.
109 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
110 # >>>
111
112 class OutputRedirector(object):
113 """ Wrapper to redirect stdout or stderr """
114 def __init__(self, fp):
115 self.fp = fp
116
117 def write(self, s):
118 self.fp.write(s)
119
120 def writelines(self, lines):
121 self.fp.writelines(lines)
122
123 def flush(self):
124 self.fp.flush()
125
126 stdout_redirector = OutputRedirector(sys.stdout)
127 stderr_redirector = OutputRedirector(sys.stderr)
128
129
130
131 # ----------------------------------------------------------------------
132 # Template
133
134 class Template_mixin(object):
135 """
136 Define a HTML template for report customerization and generation.
137
138 Overall structure of an HTML report
139
140 HTML
141 +------------------------+
142 |<html> |
143 | <head> |
144 | |
145 | STYLESHEET |
146 | +----------------+ |
147 | | | |
148 | +----------------+ |
149 | |
150 | </head> |
151 | |
152 | <body> |
153 | |
154 | HEADING |
155 | +----------------+ |
156 | | | |
157 | +----------------+ |
158 | |
159 | REPORT |
160 | +----------------+ |
161 | | | |
162 | +----------------+ |
163 | |
164 | ENDING |
165 | +----------------+ |
166 | | | |
167 | +----------------+ |
168 | |
169 | </body> |
170 |</html> |
171 +------------------------+
172 """
173
174 STATUS = {
175 0: 'pass',
176 1: 'fail',
177 2: 'error',
178 }
179
180 DEFAULT_TITLE = 'Unit Test Report'
181 DEFAULT_DESCRIPTION = ''
182
183 # ------------------------------------------------------------------------
184 # HTML Template
185
186 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
187 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
188 <html xmlns="http://www.w3.org/1999/xhtml">
189 <head>
190 <title>%(title)s</title>
191 <meta name="generator" content="%(generator)s"/>
192 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
193 %(stylesheet)s
194 </head>
195 <body>
196 <script language="javascript" type="text/javascript"><!--
197 output_list = Array();
198
199 /* level - 0:Summary; 1:Failed; 2:All */
200 function showCase(level) {
201 trs = document.getElementsByTagName("tr");
202 for (var i = 0; i < trs.length; i++) {
203 tr = trs[i];
204 id = tr.id;
205 if (id.substr(0,2) == 'ft') {
206 if (level < 1) {
207 tr.className = 'hiddenRow';
208 }
209 else {
210 tr.className = '';
211 }
212 }
213 if (id.substr(0,2) == 'pt') {
214 if (level > 1) {
215 tr.className = '';
216 }
217 else {
218 tr.className = 'hiddenRow';
219 }
220 }
221 }
222 }
223
224
225 function showClassDetail(cid, count) {
226 var id_list = Array(count);
227 var toHide = 1;
228 for (var i = 0; i < count; i++) {
229 tid0 = 't' + cid.substr(1) + '.' + (i+1);
230 tid = 'f' + tid0;
231 tr = document.getElementById(tid);
232 if (!tr) {
233 tid = 'p' + tid0;
234 tr = document.getElementById(tid);
235 }
236 id_list[i] = tid;
237 if (tr.className) {
238 toHide = 0;
239 }
240 }
241 for (var i = 0; i < count; i++) {
242 tid = id_list[i];
243 if (toHide) {
244 document.getElementById('div_'+tid).style.display = 'none'
245 document.getElementById(tid).className = 'hiddenRow';
246 }
247 else {
248 document.getElementById(tid).className = '';
249 }
250 }
251 }
252
253
254 function showTestDetail(div_id){
255 var details_div = document.getElementById(div_id)
256 var displayState = details_div.style.display
257 // alert(displayState)
258 if (displayState != 'block' ) {
259 displayState = 'block'
260 details_div.style.display = 'block'
261 }
262 else {
263 details_div.style.display = 'none'
264 }
265 }
266
267
268 function html_escape(s) {
269 s = s.replace(/&/g,'&');
270 s = s.replace(/</g,'<');
271 s = s.replace(/>/g,'>');
272 return s;
273 }
274
275 /* obsoleted by detail in <div>
276 function showOutput(id, name) {
277 var w = window.open("", //url
278 name,
279 "resizable,scrollbars,status,width=800,height=450");
280 d = w.document;
281 d.write("<pre>");
282 d.write(html_escape(output_list[id]));
283 d.write("\n");
284 d.write("<a href='javascript:window.close()'>close</a>\n");
285 d.write("</pre>\n");
286 d.close();
287 }
288 */
289 --></script>
290
291 %(heading)s
292 %(report)s
293 %(ending)s
294
295 </body>
296 </html>
297 """
298 # variables: (title, generator, stylesheet, heading, report, ending)
299
300
301 # ------------------------------------------------------------------------
302 # Stylesheet
303 #
304 # alternatively use a <link> for external style sheet, e.g.
305 # <link rel="stylesheet" href="$url" type="text/css">
306
307 STYLESHEET_TMPL = """
308 <style type="text/css" media="screen">
309 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
310 table { font-size: 100%; }
311 pre { }
312
313 /* -- heading ---------------------------------------------------------------------- */
314 h1 {
315 font-size: 16pt;
316 color: gray;
317 }
318 .heading {
319 margin-top: 0ex;
320 margin-bottom: 1ex;
321 }
322
323 .heading .attribute {
324 margin-top: 1ex;
325 margin-bottom: 0;
326 }
327
328 .heading .description {
329 margin-top: 4ex;
330 margin-bottom: 6ex;
331 }
332
333 /* -- css div popup ------------------------------------------------------------------------ */
334 a.popup_link {
335 }
336
337 a.popup_link:hover {
338 color: red;
339 }
340
341 .popup_window {
342 display: none;
343 position: relative;
344 left: 0px;
345 top: 0px;
346 /*border: solid #627173 1px; */
347 padding: 10px;
348 background-color: #E6E6D6;
349 font-family: "Lucida Console", "Courier New", Courier, monospace;
350 text-align: left;
351 font-size: 8pt;
352 width: 500px;
353 }
354
355 }
356 /* -- report ------------------------------------------------------------------------ */
357 #show_detail_line {
358 margin-top: 3ex;
359 margin-bottom: 1ex;
360 }
361 #result_table {
362 width: 80%;
363 border-collapse: collapse;
364 border: 1px solid #777;
365 }
366 #header_row {
367 font-weight: bold;
368 color: white;
369 background-color: #777;
370 }
371 #result_table td {
372 border: 1px solid #777;
373 padding: 2px;
374 }
375 #total_row { font-weight: bold; }
376 .passClass { background-color: #6c6; }
377 .failClass { background-color: #c60; }
378 .errorClass { background-color: #c00; }
379 .passCase { color: #6c6; }
380 .failCase { color: #c60; font-weight: bold; }
381 .errorCase { color: #c00; font-weight: bold; }
382 .hiddenRow { display: none; }
383 .testcase { margin-left: 2em; }
384
385
386 /* -- ending ---------------------------------------------------------------------- */
387 #ending {
388 }
389
390 </style>
391 """
392
393
394
395 # ------------------------------------------------------------------------
396 # Heading
397 #
398
399 HEADING_TMPL = """<div class='heading'>
400 <h1>%(title)s</h1>
401 %(parameters)s
402 <p class='description'>%(description)s</p>
403 </div>
404
405 """ # variables: (title, parameters, description)
406
407 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
408 """ # variables: (name, value)
409
410
411
412 # ------------------------------------------------------------------------
413 # Report
414 #
415
416 REPORT_TMPL = """
417 <p id='show_detail_line'>Show
418 <a href='javascript:showCase(0)'>Summary</a>
419 <a href='javascript:showCase(1)'>Failed</a>
420 <a href='javascript:showCase(2)'>All</a>
421 </p>
422 <table id='result_table'>
423 <colgroup>
424 <col align='left' />
425 <col align='right' />
426 <col align='right' />
427 <col align='right' />
428 <col align='right' />
429 <col align='right' />
430 </colgroup>
431 <tr id='header_row'>
432 <td>Test Group/Test case</td>
433 <td>Count</td>
434 <td>Pass</td>
435 <td>Fail</td>
436 <td>Error</td>
437 <td>View</td>
438 </tr>
439 %(test_list)s
440 <tr id='total_row'>
441 <td>Total</td>
442 <td>%(count)s</td>
443 <td>%(Pass)s</td>
444 <td>%(fail)s</td>
445 <td>%(error)s</td>
446 <td> </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'> </div>"""
507
508 # -------------------- The end of the Template class -------------------
509
510
511 TestResult = unittest.TestResult
512
513 class _TestResult(TestResult):
514 # note: _TestResult is a pure representation of results.
515 # It lacks the output and reporting ability compares to unittest._TextTestResult.
516
517 def __init__(self, verbosity=1):
518 TestResult.__init__(self)
519 self.stdout0 = None
520 self.stderr0 = None
521 self.success_count = 0
522 self.failure_count = 0
523 self.error_count = 0
524 self.verbosity = verbosity
525
526 # result is a list of result in 4 tuple
527 # (
528 # result code (0: success; 1: fail; 2: error),
529 # TestCase object,
530 # Test output (byte string),
531 # stack trace,
532 # )
533 self.result = []
534
535
536 def startTest(self, test):
537 TestResult.startTest(self, test)
538 # just one buffer for both stdout and stderr
539 self.outputBuffer = StringIO.StringIO()
540 stdout_redirector.fp = self.outputBuffer
541 stderr_redirector.fp = self.outputBuffer
542 self.stdout0 = sys.stdout
543 self.stderr0 = sys.stderr
544 sys.stdout = stdout_redirector
545 sys.stderr = stderr_redirector
546
547
548 def complete_output(self):
549 """
550 Disconnect output redirection and return buffer.
551 Safe to call multiple times.
552 """
553 if self.stdout0:
554 sys.stdout = self.stdout0
555 sys.stderr = self.stderr0
556 self.stdout0 = None
557 self.stderr0 = None
558 return self.outputBuffer.getvalue()
559
560
561 def stopTest(self, test):
562 # Usually one of addSuccess, addError or addFailure would have been called.
563 # But there are some path in unittest that would bypass this.
564 # We must disconnect stdout in stopTest(), which is guaranteed to be called.
565 self.complete_output()
566
567
568 def addSuccess(self, test):
569 self.success_count += 1
570 TestResult.addSuccess(self, test)
571 output = self.complete_output()
572 self.result.append((0, test, output, ''))
573 if self.verbosity > 1:
574 sys.stderr.write('ok ')
575 sys.stderr.write(str(test))
576 sys.stderr.write('\n')
577 else:
578 sys.stderr.write('.')
579
580 def addError(self, test, err):
581 self.error_count += 1
582 TestResult.addError(self, test, err)
583 _, _exc_str = self.errors[-1]
584 output = self.complete_output()
585 self.result.append((2, test, output, _exc_str))
586 if self.verbosity > 1:
587 sys.stderr.write('E ')
588 sys.stderr.write(str(test))
589 sys.stderr.write('\n')
590 else:
591 sys.stderr.write('E')
592
593 def addFailure(self, test, err):
594 self.failure_count += 1
595 TestResult.addFailure(self, test, err)
596 _, _exc_str = self.failures[-1]
597 output = self.complete_output()
598 self.result.append((1, test, output, _exc_str))
599 if self.verbosity > 1:
600 sys.stderr.write('F ')
601 sys.stderr.write(str(test))
602 sys.stderr.write('\n')
603 else:
604 sys.stderr.write('F')
605
606
607 class HTMLTestRunner(Template_mixin):
608 """
609 """
610 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
611 self.stream = stream
612 self.verbosity = verbosity
613 if title is None:
614 self.title = self.DEFAULT_TITLE
615 else:
616 self.title = title
617 if description is None:
618 self.description = self.DEFAULT_DESCRIPTION
619 else:
620 self.description = description
621
622 self.startTime = datetime.datetime.now()
623
624
625 def run(self, test):
626 "Run the given test case or test suite."
627 result = _TestResult(self.verbosity)
628 test(result)
629 self.stopTime = datetime.datetime.now()
630 self.generateReport(test, result)
631 print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
632 return result
633
634
635 def sortResult(self, result_list):
636 # unittest does not seems to run in any particular order.
637 # Here at least we want to group them together by class.
638 rmap = {}
639 classes = []
640 for n,t,o,e in result_list:
641 cls = t.__class__
642 if not rmap.has_key(cls):
643 rmap[cls] = []
644 classes.append(cls)
645 rmap[cls].append((n,t,o,e))
646 r = [(cls, rmap[cls]) for cls in classes]
647 return r
648
649
650 def getReportAttributes(self, result):
651 """
652 Return report attributes as a list of (name, value).
653 Override this to add custom attributes.
654 """
655 startTime = str(self.startTime)[:19]
656 duration = str(self.stopTime - self.startTime)
657 status = []
658 if result.success_count: status.append('Pass %s' % result.success_count)
659 if result.failure_count: status.append('Failure %s' % result.failure_count)
660 if result.error_count: status.append('Error %s' % result.error_count )
661 if status:
662 status = ' '.join(status)
663 else:
664 status = 'none'
665 return [
666 ('Start Time', startTime),
667 ('Duration', duration),
668 ('Status', status),
669 ]
670
671
672 def generateReport(self, test, result):
673 report_attrs = self.getReportAttributes(result)
674 generator = 'HTMLTestRunner %s' % __version__
675 stylesheet = self._generate_stylesheet()
676 heading = self._generate_heading(report_attrs)
677 report = self._generate_report(result)
678 ending = self._generate_ending()
679 output = self.HTML_TMPL % dict(
680 title = saxutils.escape(self.title),
681 generator = generator,
682 stylesheet = stylesheet,
683 heading = heading,
684 report = report,
685 ending = ending,
686 )
687 self.stream.write(output.encode('utf8'))
688
689
690 def _generate_stylesheet(self):
691 return self.STYLESHEET_TMPL
692
693
694 def _generate_heading(self, report_attrs):
695 a_lines = []
696 for name, value in report_attrs:
697 line = self.HEADING_ATTRIBUTE_TMPL % dict(
698 name = saxutils.escape(name),
699 value = saxutils.escape(value),
700 )
701 a_lines.append(line)
702 heading = self.HEADING_TMPL % dict(
703 title = saxutils.escape(self.title),
704 parameters = ''.join(a_lines),
705 description = saxutils.escape(self.description),
706 )
707 return heading
708
709
710 def _generate_report(self, result):
711 rows = []
712 sortedResult = self.sortResult(result.result)
713 for cid, (cls, cls_results) in enumerate(sortedResult):
714 # subtotal for a class
715 np = nf = ne = 0
716 for n,t,o,e in cls_results:
717 if n == 0: np += 1
718 elif n == 1: nf += 1
719 else: ne += 1
720
721 # format class description
722 if cls.__module__ == "__main__":
723 name = cls.__name__
724 else:
725 name = "%s.%s" % (cls.__module__, cls.__name__)
726 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
727 desc = doc and '%s: %s' % (name, doc) or name
728
729 row = self.REPORT_CLASS_TMPL % dict(
730 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
731 desc = desc,
732 count = np+nf+ne,
733 Pass = np,
734 fail = nf,
735 error = ne,
736 cid = 'c%s' % (cid+1),
737 )
738 rows.append(row)
739
740 for tid, (n,t,o,e) in enumerate(cls_results):
741 self._generate_report_test(rows, cid, tid, n, t, o, e)
742
743 report = self.REPORT_TMPL % dict(
744 test_list = ''.join(rows),
745 count = str(result.success_count+result.failure_count+result.error_count),
746 Pass = str(result.success_count),
747 fail = str(result.failure_count),
748 error = str(result.error_count),
749 )
750 return report
751
752
753 def _generate_report_test(self, rows, cid, tid, n, t, o, e):
754 # e.g. 'pt1.1', 'ft1.1', etc
755 has_output = bool(o or e)
756 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
757 name = t.id().split('.')[-1]
758 doc = t.shortDescription() or ""
759 desc = doc and ('%s: %s' % (name, doc)) or name
760 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
761
762 # o and e should be byte string because they are collected from stdout and stderr?
763 if isinstance(o,str):
764 # TODO: some problem with 'string_escape': it escape \n and mess up formating
765 # uo = unicode(o.encode('string_escape'))
766 uo = o.decode('latin-1')
767 else:
768 uo = o
769 if isinstance(e,str):
770 # TODO: some problem with 'string_escape': it escape \n and mess up formating
771 # ue = unicode(e.encode('string_escape'))
772 ue = e.decode('latin-1')
773 else:
774 ue = e
775
776 script = self.REPORT_TEST_OUTPUT_TMPL % dict(
777 id = tid,
778 output = saxutils.escape(uo+ue),
779 )
780
781 row = tmpl % dict(
782 tid = tid,
783 Class = (n == 0 and 'hiddenRow' or 'none'),
784 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
785 desc = desc,
786 script = script,
787 status = self.STATUS[n],
788 )
789 rows.append(row)
790 if not has_output:
791 return
792
793 def _generate_ending(self):
794 return self.ENDING_TMPL
795
796
797 ##############################################################################
798 # Facilities for running tests from the command line
799 ##############################################################################
800
801 # Note: Reuse unittest.TestProgram to launch test. In the future we may
802 # build our own launcher to support more specific command line
803 # parameters like test title, CSS, etc.
804 class TestProgram(unittest.TestProgram):
805 """
806 A variation of the unittest.TestProgram. Please refer to the base
807 class for command line parameters.
808 """
809 def runTests(self):
810 # Pick HTMLTestRunner as the default test runner.
811 # base class's testRunner parameter is not useful because it means
812 # we have to instantiate HTMLTestRunner before we know self.verbosity.
813 if self.testRunner is None:
814 self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
815 unittest.TestProgram.runTests(self)
816
817 main = TestProgram
818
819 ##############################################################################
820 # Executing this module from the command line
821 ##############################################################################
822
823 if __name__ == "__main__":
824 main(module=None)

1 """
2 A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance.
3 The simplest way to use this is to invoke its main method. E.g.
4 import unittest
5 import BSTestRunner
6 ... define your tests ...
7 if __name__ == '__main__':
8 BSTestRunner.main()
9 For more customization options, instantiates a BSTestRunner object.
10 BSTestRunner is a counterpart to unittest's TextTestRunner. E.g.
11 # output to a file
12 fp = file('my_report.html', 'wb')
13 runner = BSTestRunner.BSTestRunner(
14 stream=fp,
15 title='My unit test',
16 description='This demonstrates the report output by BSTestRunner.'
17 )
18 # Use an external stylesheet.
19 # See the Template_mixin class for more customizable options
20 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
21 # run the test
22 runner.run(my_test_suite)
23 ------------------------------------------------------------------------
24 Copyright (c) 2004-2007, Wai Yip Tung
25 Copyright (c) 2016, Eason Han
26 All rights reserved.
27 Redistribution and use in source and binary forms, with or without
28 modification, are permitted provided that the following conditions are
29 met:
30 * Redistributions of source code must retain the above copyright notice,
31 this list of conditions and the following disclaimer.
32 * Redistributions in binary form must reproduce the above copyright
33 notice, this list of conditions and the following disclaimer in the
34 documentation and/or other materials provided with the distribution.
35 * Neither the name Wai Yip Tung nor the names of its contributors may be
36 used to endorse or promote products derived from this software without
37 specific prior written permission.
38 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
39 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
40 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
41 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
42 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
43 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
44 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
45 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
46 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
47 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
48 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
49 """
50
51
52 __author__ = "Wai Yip Tung && Eason Han"
53 __version__ = "0.8.4"
54
55
56 """
57 Change History
58 Version 0.8.3
59 * Modify html style using bootstrap3.
60 Version 0.8.3
61 * Prevent crash on class or module-level exceptions (Darren Wurf).
62 Version 0.8.2
63 * Show output inline instead of popup window (Viorel Lupu).
64 Version in 0.8.1
65 * Validated XHTML (Wolfgang Borgert).
66 * Added description of test classes and test cases.
67 Version in 0.8.0
68 * Define Template_mixin class for customization.
69 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
70 Version in 0.7.1
71 * Back port to Python 2.3 (Frank Horowitz).
72 * Fix missing scroll bars in detail log (Podi).
73 """
74
75 # TODO: color stderr
76 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
77
78 import datetime
79 try:
80 from StringIO import StringIO
81 except ImportError:
82 from io import StringIO
83 import sys
84 import time
85 import unittest
86 from xml.sax import saxutils
87
88
89 # ------------------------------------------------------------------------
90 # The redirectors below are used to capture output during testing. Output
91 # sent to sys.stdout and sys.stderr are automatically captured. However
92 # in some cases sys.stdout is already cached before BSTestRunner is
93 # invoked (e.g. calling logging.basicConfig). In order to capture those
94 # output, use the redirectors for the cached stream.
95 #
96 # e.g.
97 # >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector)
98 # >>>
99
100 def to_unicode(s):
101 try:
102 return unicode(s)
103 except UnicodeDecodeError:
104 # s is non ascii byte string
105 return s.decode('unicode_escape')
106
107 class OutputRedirector(object):
108 """ Wrapper to redirect stdout or stderr """
109 def __init__(self, fp):
110 self.fp = fp
111
112 def write(self, s):
113 self.fp.write(to_unicode(s))
114
115 def writelines(self, lines):
116 lines = map(to_unicode, lines)
117 self.fp.writelines(lines)
118
119 def flush(self):
120 self.fp.flush()
121
122 stdout_redirector = OutputRedirector(sys.stdout)
123 stderr_redirector = OutputRedirector(sys.stderr)
124
125
126
127 # ----------------------------------------------------------------------
128 # Template
129
130 class Template_mixin(object):
131 """
132 Define a HTML template for report customerization and generation.
133 Overall structure of an HTML report
134 HTML
135 +------------------------+
136 |<html> |
137 | <head> |
138 | |
139 | STYLESHEET |
140 | +----------------+ |
141 | | | |
142 | +----------------+ |
143 | |
144 | </head> |
145 | |
146 | <body> |
147 | |
148 | HEADING |
149 | +----------------+ |
150 | | | |
151 | +----------------+ |
152 | |
153 | REPORT |
154 | +----------------+ |
155 | | | |
156 | +----------------+ |
157 | |
158 | ENDING |
159 | +----------------+ |
160 | | | |
161 | +----------------+ |
162 | |
163 | </body> |
164 |</html> |
165 +------------------------+
166 """
167
168 STATUS = {
169 0: 'pass',
170 1: 'fail',
171 2: 'error',
172 }
173
174 DEFAULT_TITLE = 'Unit Test Report'
175 DEFAULT_DESCRIPTION = ''
176
177 # ------------------------------------------------------------------------
178 # HTML Template
179
180 HTML_TMPL = r"""<!DOCTYPE html>
181 <html lang="zh-cn">
182 <head>
183 <meta charset="utf-8">
184 <meta http-equiv="X-UA-Compatible" content="IE=edge">
185 <meta name="viewport" content="width=device-width, initial-scale=1">
186 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
187 <title>%(title)s</title>
188 <meta name="generator" content="%(generator)s"/>
189 <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
190 %(stylesheet)s
191 <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
192 <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
193 <!--[if lt IE 9]>
194 <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
195 <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
196 <![endif]-->
197 </head>
198 <body>
199 <script language="javascript" type="text/javascript"><!--
200 output_list = Array();
201 /* level - 0:Summary; 1:Failed; 2:All */
202 function showCase(level) {
203 trs = document.getElementsByTagName("tr");
204 for (var i = 0; i < trs.length; i++) {
205 tr = trs[i];
206 id = tr.id;
207 if (id.substr(0,2) == 'ft') {
208 if (level < 1) {
209 tr.className = 'hiddenRow';
210 }
211 else {
212 tr.className = '';
213 }
214 }
215 if (id.substr(0,2) == 'pt') {
216 if (level > 1) {
217 tr.className = '';
218 }
219 else {
220 tr.className = 'hiddenRow';
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 function showTestDetail(div_id){
253 var details_div = document.getElementById(div_id)
254 var displayState = details_div.style.display
255 // alert(displayState)
256 if (displayState != 'block' ) {
257 displayState = 'block'
258 details_div.style.display = 'block'
259 }
260 else {
261 details_div.style.display = 'none'
262 }
263 }
264 function html_escape(s) {
265 s = s.replace(/&/g,'&');
266 s = s.replace(/</g,'<');
267 s = s.replace(/>/g,'>');
268 return s;
269 }
270 /* obsoleted by detail in <div>
271 function showOutput(id, name) {
272 var w = window.open("", //url
273 name,
274 "resizable,scrollbars,status,width=800,height=450");
275 d = w.document;
276 d.write("<pre>");
277 d.write(html_escape(output_list[id]));
278 d.write("\n");
279 d.write("<a href='javascript:window.close()'>close</a>\n");
280 d.write("</pre>\n");
281 d.close();
282 }
283 */
284 --></script>
285 <div class="container">
286 %(heading)s
287 %(report)s
288 %(ending)s
289 </div>
290 </body>
291 </html>
292 """
293 # variables: (title, generator, stylesheet, heading, report, ending)
294
295
296 # ------------------------------------------------------------------------
297 # Stylesheet
298 #
299 # alternatively use a <link> for external style sheet, e.g.
300 # <link rel="stylesheet" href="$url" type="text/css">
301
302 STYLESHEET_TMPL = """
303 <style type="text/css" media="screen">
304 /* -- css div popup ------------------------------------------------------------------------ */
305 .popup_window {
306 display: none;
307 position: relative;
308 left: 0px;
309 top: 0px;
310 /*border: solid #627173 1px; */
311 padding: 10px;
312 background-color: #99CCFF;
313 font-family: "Lucida Console", "Courier New", Courier, monospace;
314 text-align: left;
315 font-size: 10pt;
316 width: 500px;
317 }
318 /* -- report ------------------------------------------------------------------------ */
319 #show_detail_line .label {
320 font-size: 85%;
321 cursor: pointer;
322 }
323 #show_detail_line {
324 margin: 2em auto 1em auto;
325 }
326 #total_row { font-weight: bold; }
327 .hiddenRow { display: none; }
328 .testcase { margin-left: 2em; }
329 </style>
330 """
331
332
333
334 # ------------------------------------------------------------------------
335 # Heading
336 #
337
338 HEADING_TMPL = """<div class='heading'>
339 <h1>%(title)s</h1>
340 %(parameters)s
341 <p class='description'>%(description)s</p>
342 </div>
343 """ # variables: (title, parameters, description)
344
345 HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>
346 """ # variables: (name, value)
347
348
349
350 # ------------------------------------------------------------------------
351 # Report
352 #
353
354 REPORT_TMPL = """
355 <p id='show_detail_line'>
356 <span class="label label-primary" onclick="showCase(0)">Summary</span>
357 <span class="label label-danger" onclick="showCase(1)">Failed</span>
358 <span class="label label-default" onclick="showCase(2)">All</span>
359 </p>
360 <table id='result_table' class="table">
361 <thead>
362 <tr id='header_row'>
363 <th>Test Group/Test case</td>
364 <th>Count</td>
365 <th>Pass</td>
366 <th>Fail</td>
367 <th>Error</td>
368 <th>View</td>
369 </tr>
370 </thead>
371 <tbody>
372 %(test_list)s
373 </tbody>
374 <tfoot>
375 <tr id='total_row'>
376 <td>Total</td>
377 <td>%(count)s</td>
378 <td class="text text-success">%(Pass)s</td>
379 <td class="text text-danger">%(fail)s</td>
380 <td class="text text-warning">%(error)s</td>
381 <td> </td>
382 </tr>
383 </tfoot>
384 </table>
385 """ # variables: (test_list, count, Pass, fail, error)
386
387 REPORT_CLASS_TMPL = r"""
388 <tr class='%(style)s'>
389 <td>%(desc)s</td>
390 <td>%(count)s</td>
391 <td>%(Pass)s</td>
392 <td>%(fail)s</td>
393 <td>%(error)s</td>
394 <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
395 </tr>
396 """ # variables: (style, desc, count, Pass, fail, error, cid)
397
398
399 REPORT_TEST_WITH_OUTPUT_TMPL = r"""
400 <tr id='%(tid)s' class='%(Class)s'>
401 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
402 <td colspan='5' align='center'>
403 <!--css div popup start-->
404 <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
405 %(status)s</a>
406 <div id='div_%(tid)s' class="popup_window">
407 <div style='text-align: right;cursor:pointer'>
408 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
409 [x]</a>
410 </div>
411 <pre>
412 %(script)s
413 </pre>
414 </div>
415 <!--css div popup end-->
416 </td>
417 </tr>
418 """ # variables: (tid, Class, style, desc, status)
419
420
421 REPORT_TEST_NO_OUTPUT_TMPL = r"""
422 <tr id='%(tid)s' class='%(Class)s'>
423 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
424 <td colspan='5' align='center'>%(status)s</td>
425 </tr>
426 """ # variables: (tid, Class, style, desc, status)
427
428
429 REPORT_TEST_OUTPUT_TMPL = r"""
430 %(id)s: %(output)s
431 """ # variables: (id, output)
432
433
434
435 # ------------------------------------------------------------------------
436 # ENDING
437 #
438
439 ENDING_TMPL = """<div id='ending'> </div>"""
440
441 # -------------------- The end of the Template class -------------------
442
443
444 TestResult = unittest.TestResult
445
446 class _TestResult(TestResult):
447 # note: _TestResult is a pure representation of results.
448 # It lacks the output and reporting ability compares to unittest._TextTestResult.
449
450 def __init__(self, verbosity=1):
451 TestResult.__init__(self)
452 self.outputBuffer = StringIO()
453 self.stdout0 = None
454 self.stderr0 = None
455 self.success_count = 0
456 self.failure_count = 0
457 self.error_count = 0
458 self.verbosity = verbosity
459
460 # result is a list of result in 4 tuple
461 # (
462 # result code (0: success; 1: fail; 2: error),
463 # TestCase object,
464 # Test output (byte string),
465 # stack trace,
466 # )
467 self.result = []
468
469
470 def startTest(self, test):
471 TestResult.startTest(self, test)
472 # just one buffer for both stdout and stderr
473 stdout_redirector.fp = self.outputBuffer
474 stderr_redirector.fp = self.outputBuffer
475 self.stdout0 = sys.stdout
476 self.stderr0 = sys.stderr
477 sys.stdout = stdout_redirector
478 sys.stderr = stderr_redirector
479
480
481 def complete_output(self):
482 """
483 Disconnect output redirection and return buffer.
484 Safe to call multiple times.
485 """
486 if self.stdout0:
487 sys.stdout = self.stdout0
488 sys.stderr = self.stderr0
489 self.stdout0 = None
490 self.stderr0 = None
491 return self.outputBuffer.getvalue()
492
493
494 def stopTest(self, test):
495 # Usually one of addSuccess, addError or addFailure would have been called.
496 # But there are some path in unittest that would bypass this.
497 # We must disconnect stdout in stopTest(), which is guaranteed to be called.
498 self.complete_output()
499
500
501 def addSuccess(self, test):
502 self.success_count += 1
503 TestResult.addSuccess(self, test)
504 output = self.complete_output()
505 self.result.append((0, test, output, ''))
506 if self.verbosity > 1:
507 sys.stderr.write('ok ')
508 sys.stderr.write(str(test))
509 sys.stderr.write('\n')
510 else:
511 sys.stderr.write('.')
512
513 def addError(self, test, err):
514 self.error_count += 1
515 TestResult.addError(self, test, err)
516 _, _exc_str = self.errors[-1]
517 output = self.complete_output()
518 self.result.append((2, test, output, _exc_str))
519 if self.verbosity > 1:
520 sys.stderr.write('E ')
521 sys.stderr.write(str(test))
522 sys.stderr.write('\n')
523 else:
524 sys.stderr.write('E')
525
526 def addFailure(self, test, err):
527 self.failure_count += 1
528 TestResult.addFailure(self, test, err)
529 _, _exc_str = self.failures[-1]
530 output = self.complete_output()
531 self.result.append((1, test, output, _exc_str))
532 if self.verbosity > 1:
533 sys.stderr.write('F ')
534 sys.stderr.write(str(test))
535 sys.stderr.write('\n')
536 else:
537 sys.stderr.write('F')
538
539
540 class BSTestRunner(Template_mixin):
541 """
542 """
543 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
544 self.stream = stream
545 self.verbosity = verbosity
546 if title is None:
547 self.title = self.DEFAULT_TITLE
548 else:
549 self.title = title
550 if description is None:
551 self.description = self.DEFAULT_DESCRIPTION
552 else:
553 self.description = description
554
555 self.startTime = datetime.datetime.now()
556
557
558 def run(self, test):
559 "Run the given test case or test suite."
560 result = _TestResult(self.verbosity)
561 test(result)
562 self.stopTime = datetime.datetime.now()
563 self.generateReport(test, result)
564 # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
565 sys.stderr.write('\nTime Elapsed: %s' % (self.stopTime-self.startTime))
566 return result
567
568
569 def sortResult(self, result_list):
570 # unittest does not seems to run in any particular order.
571 # Here at least we want to group them together by class.
572 rmap = {}
573 classes = []
574 for n,t,o,e in result_list:
575 cls = t.__class__
576 # if not rmap.has_key(cls):
577 if not cls in rmap:
578 rmap[cls] = []
579 classes.append(cls)
580 rmap[cls].append((n,t,o,e))
581 r = [(cls, rmap[cls]) for cls in classes]
582 return r
583
584
585 def getReportAttributes(self, result):
586 """
587 Return report attributes as a list of (name, value).
588 Override this to add custom attributes.
589 """
590 startTime = str(self.startTime)[:19]
591 duration = str(self.stopTime - self.startTime)
592 status = []
593 if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>' % result.success_count)
594 if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
595 if result.error_count: status.append('<span class="text text-warning">Error <strong>%s</strong></span>' % result.error_count )
596 if status:
597 status = ' '.join(status)
598 else:
599 status = 'none'
600 return [
601 ('Start Time', startTime),
602 ('Duration', duration),
603 ('Status', status),
604 ]
605
606
607 def generateReport(self, test, result):
608 report_attrs = self.getReportAttributes(result)
609 generator = 'BSTestRunner %s' % __version__
610 stylesheet = self._generate_stylesheet()
611 heading = self._generate_heading(report_attrs)
612 report = self._generate_report(result)
613 ending = self._generate_ending()
614 output = self.HTML_TMPL % dict(
615 title = saxutils.escape(self.title),
616 generator = generator,
617 stylesheet = stylesheet,
618 heading = heading,
619 report = report,
620 ending = ending,
621 )
622 try:
623 self.stream.write(output.encode('utf8'))
624 except:
625 self.stream.write(output)
626
627
628 def _generate_stylesheet(self):
629 return self.STYLESHEET_TMPL
630
631
632 def _generate_heading(self, report_attrs):
633 a_lines = []
634 for name, value in report_attrs:
635 line = self.HEADING_ATTRIBUTE_TMPL % dict(
636 # name = saxutils.escape(name),
637 # value = saxutils.escape(value),
638 name = name,
639 value = value,
640 )
641 a_lines.append(line)
642 heading = self.HEADING_TMPL % dict(
643 title = saxutils.escape(self.title),
644 parameters = ''.join(a_lines),
645 description = saxutils.escape(self.description),
646 )
647 return heading
648
649
650 def _generate_report(self, result):
651 rows = []
652 sortedResult = self.sortResult(result.result)
653 for cid, (cls, cls_results) in enumerate(sortedResult):
654 # subtotal for a class
655 np = nf = ne = 0
656 for n,t,o,e in cls_results:
657 if n == 0: np += 1
658 elif n == 1: nf += 1
659 else: ne += 1
660
661 # format class description
662 if cls.__module__ == "__main__":
663 name = cls.__name__
664 else:
665 name = "%s.%s" % (cls.__module__, cls.__name__)
666 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
667 desc = doc and '%s: %s' % (name, doc) or name
668
669 row = self.REPORT_CLASS_TMPL % dict(
670 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
671 desc = desc,
672 count = np+nf+ne,
673 Pass = np,
674 fail = nf,
675 error = ne,
676 cid = 'c%s' % (cid+1),
677 )
678 rows.append(row)
679
680 for tid, (n,t,o,e) in enumerate(cls_results):
681 self._generate_report_test(rows, cid, tid, n, t, o, e)
682
683 report = self.REPORT_TMPL % dict(
684 test_list = ''.join(rows),
685 count = str(result.success_count+result.failure_count+result.error_count),
686 Pass = str(result.success_count),
687 fail = str(result.failure_count),
688 error = str(result.error_count),
689 )
690 return report
691
692
693 def _generate_report_test(self, rows, cid, tid, n, t, o, e):
694 # e.g. 'pt1.1', 'ft1.1', etc
695 has_output = bool(o or e)
696 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
697 name = t.id().split('.')[-1]
698 doc = t.shortDescription() or ""
699 desc = doc and ('%s: %s' % (name, doc)) or name
700 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
701
702 # o and e should be byte string because they are collected from stdout and stderr?
703 if isinstance(o,str):
704 # TODO: some problem with 'string_escape': it escape \n and mess up formating
705 # uo = unicode(o.encode('string_escape'))
706 try:
707 uo = o.decode('latin-1')
708 except:
709 uo = o
710 else:
711 uo = o
712 if isinstance(e,str):
713 # TODO: some problem with 'string_escape': it escape \n and mess up formating
714 # ue = unicode(e.encode('string_escape'))
715 try:
716 ue = e.decode('latin-1')
717 except:
718 ue = e
719 else:
720 ue = e
721
722 script = self.REPORT_TEST_OUTPUT_TMPL % dict(
723 id = tid,
724 output = saxutils.escape(uo+ue),
725 )
726
727 row = tmpl % dict(
728 tid = tid,
729 # Class = (n == 0 and 'hiddenRow' or 'none'),
730 Class = (n == 0 and 'hiddenRow' or 'text text-success'),
731 # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
732 style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
733 desc = desc,
734 script = script,
735 status = self.STATUS[n],
736 )
737 rows.append(row)
738 if not has_output:
739 return
740
741 def _generate_ending(self):
742 return self.ENDING_TMPL
743
744
745 ##############################################################################
746 # Facilities for running tests from the command line
747 ##############################################################################
748
749 # Note: Reuse unittest.TestProgram to launch test. In the future we may
750 # build our own launcher to support more specific command line
751 # parameters like test title, CSS, etc.
752 class TestProgram(unittest.TestProgram):
753 """
754 A variation of the unittest.TestProgram. Please refer to the base
755 class for command line parameters.
756 """
757 def runTests(self):
758 # Pick BSTestRunner as the default test runner.
759 # base class's testRunner parameter is not useful because it means
760 # we have to instantiate BSTestRunner before we know self.verbosity.
761 if self.testRunner is None:
762 self.testRunner = BSTestRunner(verbosity=self.verbosity)
763 unittest.TestProgram.runTests(self)
764
765 main = TestProgram
766
767 ##############################################################################
768 # Executing this module from the command line
769 ##############################################################################
770
771 if __name__ == "__main__":
772 main(module=None)
see also:
import webbrowserimport unittestimport HTMLTestRunnerimport BSTestRunnerclass TestStringMethods(unittest.TestCase): def test_upper(self): u"""判断 foo.upper() 是否等于 FOO""" self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): u""" 判断 Foo 是否为大写形式 """ self.assertTrue('Foo'.isupper())if __name__ == '__main__': suite = unittest.makeSuite(TestStringMethods) f1 = open('result1.html', 'wb') f2 = open('result2.html', 'wb') HTMLTestRunner.HTMLTestRunner( stream=f1, title=u'HTMLTestRunner版本关于upper的测试报告', description=u'判断upper的测试用例执行情况').run(suite) suite = unittest.makeSuite(TestStringMethods) BSTestRunner.BSTestRunner( stream=f2, title=u'BSTestRunner版本关于upper的测试报告', description=u'判断upper的测试用例执行情况').run(suite) f1.close() f2.close() webbrowser.open('result1.html') webbrowser.open('result2.html')