Salome HOME
relax constraint about openssl for windows - Python embeds already ssl
[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 class OutputRedirector(object):
113     """ Wrapper to redirect stdout or stderr """
114     def __init__(self, fp):
115         self.fp = fp
116
117     def write(self, s):
118         self.fp.write(s)
119
120     def writelines(self, lines):
121         self.fp.writelines(lines)
122
123     def flush(self):
124         self.fp.flush()
125
126 stdout_redirector = OutputRedirector(sys.stdout)
127 stderr_redirector = OutputRedirector(sys.stderr)
128
129
130
131 # ----------------------------------------------------------------------
132 # Template
133
134 class Template_mixin(object):
135     """
136     Define a HTML template for report customerization and generation.
137
138     Overall structure of an HTML report
139
140     HTML
141     +------------------------+
142     |<html>                  |
143     |  <head>                |
144     |                        |
145     |   STYLESHEET           |
146     |   +----------------+   |
147     |   |                |   |
148     |   +----------------+   |
149     |                        |
150     |  </head>               |
151     |                        |
152     |  <body>                |
153     |                        |
154     |   HEADING              |
155     |   +----------------+   |
156     |   |                |   |
157     |   +----------------+   |
158     |                        |
159     |   REPORT               |
160     |   +----------------+   |
161     |   |                |   |
162     |   +----------------+   |
163     |                        |
164     |   ENDING               |
165     |   +----------------+   |
166     |   |                |   |
167     |   +----------------+   |
168     |                        |
169     |  </body>               |
170     |</html>                 |
171     +------------------------+
172     """
173
174     STATUS = {
175     0: 'pass',
176     1: 'fail',
177     2: 'error',
178     }
179
180     DEFAULT_TITLE = 'Unit Test Report'
181     DEFAULT_DESCRIPTION = ''
182
183     # ------------------------------------------------------------------------
184     # HTML Template
185
186     HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
187 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
188 <html xmlns="http://www.w3.org/1999/xhtml">
189 <head>
190     <title>%(title)s</title>
191     <meta name="generator" content="%(generator)s"/>
192     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
193     %(stylesheet)s
194 </head>
195 <body>
196 <script language="javascript" type="text/javascript"><!--
197 output_list = Array();
198
199 /* level - 0:Summary; 1:Failed; 2:All */
200 function showCase(level) {
201     trs = document.getElementsByTagName("tr");
202     for (var i = 0; i < trs.length; i++) {
203         tr = trs[i];
204         id = tr.id;
205         if (id.substr(0,2) == 'ft') {
206             if (level < 1) {
207                 tr.className = 'hiddenRow';
208             }
209             else {
210                 tr.className = '';
211             }
212         }
213         if (id.substr(0,2) == 'pt') {
214             if (level > 1) {
215                 tr.className = '';
216             }
217             else {
218                 tr.className = 'hiddenRow';
219             }
220         }
221     }
222 }
223
224
225 function showClassDetail(cid, count) {
226     var id_list = Array(count);
227     var toHide = 1;
228     for (var i = 0; i < count; i++) {
229         tid0 = 't' + cid.substr(1) + '.' + (i+1);
230         tid = 'f' + tid0;
231         tr = document.getElementById(tid);
232         if (!tr) {
233             tid = 'p' + tid0;
234             tr = document.getElementById(tid);
235         }
236         id_list[i] = tid;
237         if (tr.className) {
238             toHide = 0;
239         }
240     }
241     for (var i = 0; i < count; i++) {
242         tid = id_list[i];
243         if (toHide) {
244             document.getElementById('div_'+tid).style.display = 'none'
245             document.getElementById(tid).className = 'hiddenRow';
246         }
247         else {
248             document.getElementById(tid).className = '';
249         }
250     }
251 }
252
253
254 function showTestDetail(div_id){
255     var details_div = document.getElementById(div_id)
256     var displayState = details_div.style.display
257     // alert(displayState)
258     if (displayState != 'block' ) {
259         displayState = 'block'
260         details_div.style.display = 'block'
261     }
262     else {
263         details_div.style.display = 'none'
264     }
265 }
266
267
268 function html_escape(s) {
269     s = s.replace(/&/g,'&amp;');
270     s = s.replace(/</g,'&lt;');
271     s = s.replace(/>/g,'&gt;');
272     return s;
273 }
274
275 /* obsoleted by detail in <div>
276 function showOutput(id, name) {
277     var w = window.open("", //url
278                     name,
279                     "resizable,scrollbars,status,width=800,height=450");
280     d = w.document;
281     d.write("<pre>");
282     d.write(html_escape(output_list[id]));
283     d.write("\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>&nbsp;</td>
447 </tr>
448 </table>
449 """ # variables: (test_list, count, Pass, fail, error)
450
451     REPORT_CLASS_TMPL = r"""
452 <tr class='%(style)s'>
453     <td>%(desc)s</td>
454     <td>%(count)s</td>
455     <td>%(Pass)s</td>
456     <td>%(fail)s</td>
457     <td>%(error)s</td>
458     <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
459 </tr>
460 """ # variables: (style, desc, count, Pass, fail, error, cid)
461
462
463     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
464 <tr id='%(tid)s' class='%(Class)s'>
465     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
466     <td colspan='5' align='center'>
467
468     <!--css div popup start-->
469     <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
470         %(status)s</a>
471
472     <div id='div_%(tid)s' class="popup_window">
473         <div style='text-align: right; color:red;cursor:pointer'>
474         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
475            [x]</a>
476         </div>
477         <pre>
478         %(script)s
479         </pre>
480     </div>
481     <!--css div popup end-->
482
483     </td>
484 </tr>
485 """ # variables: (tid, Class, style, desc, status)
486
487
488     REPORT_TEST_NO_OUTPUT_TMPL = r"""
489 <tr id='%(tid)s' class='%(Class)s'>
490     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
491     <td colspan='5' align='center'>%(status)s</td>
492 </tr>
493 """ # variables: (tid, Class, style, desc, status)
494
495
496     REPORT_TEST_OUTPUT_TMPL = r"""
497 %(id)s: %(output)s
498 """ # variables: (id, output)
499
500
501
502     # ------------------------------------------------------------------------
503     # ENDING
504     #
505
506     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
507
508 # -------------------- The end of the Template class -------------------
509
510
511 TestResult = unittest.TestResult
512
513 class _TestResult(TestResult):
514     # note: _TestResult is a pure representation of results.
515     # It lacks the output and reporting ability compares to unittest._TextTestResult.
516
517     def __init__(self, verbosity=1):
518         TestResult.__init__(self)
519         self.stdout0 = None
520         self.stderr0 = None
521         self.success_count = 0
522         self.failure_count = 0
523         self.error_count = 0
524         self.verbosity = verbosity
525
526         # result is a list of result in 4 tuple
527         # (
528         #   result code (0: success; 1: fail; 2: error),
529         #   TestCase object,
530         #   Test output (byte string),
531         #   stack trace,
532         # )
533         self.result = []
534
535
536     def startTest(self, test):
537         TestResult.startTest(self, test)
538         # just one buffer for both stdout and stderr
539         self.outputBuffer = 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 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.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)