~unifield-team/unifield-wm/us-826

« back to all changes in this revision

Viewing changes to unifield_tests/HTMLTestRunner.py

  • Committer: Quentin THEURET
  • Date: 2016-03-04 12:15:00 UTC
  • Revision ID: qt@tempo-consulting.fr-20160304121500-u2ay8zrf83ih9fu3
US-826 [IMP] Change the way to check if products is not consistent on add multiple line wizard

Show diffs side-by-side

added added

removed removed

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