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