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
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
91
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
103
104
105
106
107
108
109
110
111
113 """ Wrapper to redirect stdout or stderr """
116
119
122
125
126 stdout_redirector = OutputRedirector(sys.stdout)
127 stderr_redirector = OutputRedirector(sys.stderr)
128
129
130
131
132
133
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
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
299
300
301
302
303
304
305
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
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 """
406
407 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
408 """
409
410
411
412
413
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 """
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 """
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 """
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 """
494
495
496 REPORT_TEST_OUTPUT_TMPL = r"""
497 %(id)s: %(output)s
498 """
499
500
501
502
503
504
505
506 ENDING_TMPL = """<div id='ending'> </div>"""
507
508
509
510
511 TestResult = unittest.TestResult
512
514
515
516
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
527
528
529
530
531
532
533 self.result = []
534
535
546
547
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
566
567
579
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
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
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
636
637
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
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
688
689
692
693
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
711 rows = []
712 sortedResult = self.sortResult(result.result)
713 for cid, (cls, cls_results) in enumerate(sortedResult):
714
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
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
754
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
763 if isinstance(o,str):
764
765
766 uo = o.decode('latin-1')
767 else:
768 uo = o
769 if isinstance(e,str):
770
771
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
795
796
797
798
799
800
801
802
803
805 """
806 A variation of the unittest.TestProgram. Please refer to the base
807 class for command line parameters.
808 """
816
817 main = TestProgram
818
819
820
821
822
823 if __name__ == "__main__":
824 main(module=None)
825