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