]> SALOME platform Git repositories - tools/eficas.git/commitdiff
Salome HOME
CCAR: ajout HTMLTestRunner
authoreficas <>
Sun, 26 Feb 2006 14:08:44 +0000 (14:08 +0000)
committereficas <>
Sun, 26 Feb 2006 14:08:44 +0000 (14:08 +0000)
Tests/HTMLTestRunner.py [new file with mode: 0644]
Tests/run.py

diff --git a/Tests/HTMLTestRunner.py b/Tests/HTMLTestRunner.py
new file mode 100644 (file)
index 0000000..e59a8ef
--- /dev/null
@@ -0,0 +1,538 @@
+"""
+Copyright (c) 2004, Wai Yip Tung
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+* Neither the name of the Wai Yip Tung nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+A TestRunner for use with the Python unit testing framework. It
+generates a HTML report to show the result at a glance.
+
+The simplest way to use this is to invoke its main method. E.g.
+
+    import unittest
+    import HTMLTestRunner
+
+    ... define your tests ...
+
+    if __name__ == '__main__':
+        HTMLTestRunner.main()
+
+It defines the class HTMLTestRunner, which is a counterpart of unittest's
+TextTestRunner. You can also instantiates a HTMLTestRunner object for
+finer control.
+"""
+
+# URL: http://tungwaiyip.info/software
+
+__author__ = "Wai Yip Tung"
+__version__ = "0.7"
+
+
+# TOOD: need to make sure all HTML and JavaScript blocks are properly escaped!
+# TODO: allow link to custom CSS
+# TODO: color stderr
+# TODO: simplify javascript using ,ore than 1 class in the class attribute?
+
+import datetime
+import string
+import StringIO
+import sys
+import time
+import unittest
+from xml.sax import saxutils
+
+
+# ------------------------------------------------------------------------
+# The redirectors below is used to capture output during testing. Output
+# sent to sys.stdout and sys.stderr are automatically captured. However
+# in some cases sys.stdout is already cached before HTMLTestRunner is
+# invoked (e.g. calling logging.basicConfig). In order to capture those
+# output, use the redirectors for the cached stream.
+#
+# e.g.
+#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
+#   >>>
+
+class OutputRedirector(object):
+    """ Wrapper to redirect stdout or stderr """
+    def __init__(self, fp):
+        self.fp = fp
+
+    def write(self, s):
+        self.fp.write(s)
+
+    def writelines(self, lines):
+        self.fp.writelines(lines)
+
+    def flush(self):
+        self.fp.flush()
+
+stdout_redirector = OutputRedirector(sys.stdout)
+stderr_redirector = OutputRedirector(sys.stderr)
+
+
+
+# ----------------------------------------------------------------------
+# Template
+
+STATUS = {
+0: 'pass',
+1: 'fail',
+2: 'error',
+}
+
+
+CSS = """
+<style>
+body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
+table       { font-size: 100%; }
+pre         { }
+h1          { }
+.heading    {
+    margin-top: 0ex;
+    margin-bottom: 1ex;
+}
+#show_detail_line {
+    margin-top: 3ex;
+    margin-bottom: 1ex;
+}
+#result_table {
+    width: 80%;
+    border-collapse: collapse;
+    border: medium solid #777;
+}
+#result_table td {
+    border: thin solid #777;
+    padding: 2px;
+}
+#header_row {
+    font-weight: bold;
+    color: white;
+    background-color: #777;
+}
+#total_row  { font-weight: bold; }
+.passClass  { background-color: #6c6; }
+.failClass  { background-color: #c60; }
+.errorClass { background-color: #c00; }
+.passCase   { color: #6c6; }
+.failCase   { color: #c60; font-weight: bold; }
+.errorCase  { color: #c00; font-weight: bold; }
+.hiddenRow  { display: none; }
+.testcase   { margin-left: 2em; }
+#btm_filler { margin-top: 50%; }
+</style>
+"""
+
+# currently not used
+CSS_LINK = '<link rel="stylesheet" href="$url" type="text/css">\n'
+
+
+HTML_TMPL = string.Template(r"""
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+    <title>$title</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    $css
+</head>
+<body>
+<script>
+output_list = Array();
+
+/* level - 0:Summary; 1:Failed; 2:All */
+function showCase(level) {
+    trs = document.getElementsByTagName("tr");
+    for (var i = 0; i < trs.length; i++) {
+        tr = trs[i];
+        id = tr.id;
+        if (id.substr(0,2) == 'ft') {
+            if (level < 1) {
+                tr.className = 'hiddenRow';
+            }
+            else {
+                tr.className = '';
+            }
+        }
+        if (id.substr(0,2) == 'pt') {
+            if (level > 1) {
+                tr.className = '';
+            }
+            else {
+                tr.className = 'hiddenRow';
+            }
+        }
+    }
+}
+
+function showClassDetail(cid, count) {
+    var id_list = Array(count);
+    var toHide = 1;
+    for (var i = 0; i < count; i++) {
+        tid0 = 't' + cid.substr(1) + '.' + (i+1);
+        tid = 'f' + tid0;
+        tr = document.getElementById(tid);
+        if (!tr) {
+            tid = 'p' + tid0;
+            tr = document.getElementById(tid);
+        }
+        id_list[i] = tid;
+        if (tr.className) {
+            toHide = 0;
+        }
+    }
+    for (var i = 0; i < count; i++) {
+        tid = id_list[i];
+        if (toHide) {
+            document.getElementById(tid).className = 'hiddenRow';
+        }
+        else {
+            document.getElementById(tid).className = '';
+        }
+    }
+}
+
+function showOutput(id, name) {
+    w = window.open("", //url
+                    name,
+                    "resizable,status,width=800,height=450");
+    d = w.document;
+    d.write("<pre>");
+    d.write(output_list[id]);
+    d.write("\n");
+    d.write("<a href='javascript:window.close()'>close</a>\n");
+    d.write("</pre>\n");
+    d.close();
+}
+
+</script>
+
+<h1>$description</h1>
+<p class='heading'><strong>Time:</strong> $time</p>
+<p class='heading'><strong>Status:</strong> $status</p>
+<p id='show_detail_line'>Show
+<a href='javascript:showCase(0)'>Summary</a>
+<a href='javascript:showCase(1)'>Failed</a>
+<a href='javascript:showCase(2)'>All</a>
+</p>
+<table id='result_table'>
+<colgroup>
+<col align='left' />
+<col align='right' />
+<col align='right' />
+<col align='right' />
+<col align='right' />
+<col align='right' />
+</colgroup>
+<tr id='header_row'>
+    <td>Class/Test case</td>
+    <td>Count</td>
+    <td>Pass</td>
+    <td>Fail</td>
+    <td>Error</td>
+    <td>View</td>
+</tr>
+$tests
+<tr id='total_row'>
+    <td>Total</td>
+    <td>$count</td>
+    <td>$Pass</td>
+    <td>$fail</td>
+    <td>$error</td>
+    <td>&nbsp;</td>
+</tr>
+</table>
+<div id='btm_filler' />
+</body>
+</html>
+""")
+
+CLASS_TMPL = string.Template(r"""
+<tr class='$style'>
+    <td>$name</td>
+    <td>$count</td>
+    <td>$Pass</td>
+    <td>$fail</td>
+    <td>$error</td>
+    <td><a href="javascript:showClassDetail('$cid',$count)">Detail</a></td>
+</tr>
+""")
+
+TEST_TMPL = string.Template(r"""
+<tr id='$tid' class='$Class'>
+    <td class='$style'><div class='testcase'>$name<div></td>
+    <td colspan='5' align='center'><a href="javascript:showOutput('$tid', '$name')">$status</a></td>
+</tr>
+""")
+
+TEST_TMPL_NO_OUTPUT = string.Template(r"""
+<tr id='$tid' class='$Class'>
+    <td class='$style'><div class='testcase'>$name<div></td>
+    <td colspan='5' align='center'>$status</td>
+</tr>
+""")
+
+TEST_OUTPUT_TMPL = string.Template(r"""
+<script>output_list['$id'] = '$output';</script>
+""")
+
+
+# ----------------------------------------------------------------------
+
+TestResult = unittest.TestResult
+
+class _TestResult(TestResult):
+    # note: _TestResult is a pure representation of results.
+    # It lacks the output and reporting ability compares to unittest._TextTestResult.
+
+    def __init__(self, verbosity=1):
+        TestResult.__init__(self)
+        self.stdout0 = None
+        self.stderr0 = None
+        self.verbosity = verbosity
+
+        # result is a list of result in 4 tuple
+        # (
+        #   result code (0: success; 1: fail; 2: error),
+        #   TestCase object,
+        #   Test output (byte string),
+        #   stack trace,
+        # )
+        self.result = []
+
+
+    def startTest(self, test):
+        TestResult.startTest(self, test)
+        # just one buffer for both stdout and stderr
+        self.outputBuffer = StringIO.StringIO()
+        stdout_redirector.fp = self.outputBuffer
+        stderr_redirector.fp = self.outputBuffer
+        self.stdout0 = sys.stdout
+        self.stderr0 = sys.stderr
+        sys.stdout = stdout_redirector
+        sys.stderr = stderr_redirector
+
+
+    def complete_output(self):
+        """
+        Disconnect output redirection and return buffer.
+        Safe to call multiple times.
+        """
+        if self.stdout0:
+            sys.stdout = self.stdout0
+            sys.stderr = self.stderr0
+            self.stdout0 = None
+            self.stderr0 = None
+        return self.outputBuffer.getvalue()
+
+
+    def stopTest(self, test):
+        # Usually one of addSuccess, addError or addFailure would have been called.
+        # But there are some path in unittest that would bypass this.
+        # We must disconnect stdout in stopTest(), which is guaranteed to be called.
+        self.complete_output()
+
+
+    def addSuccess(self, test):
+        TestResult.addSuccess(self, test)
+        output = self.complete_output()
+        self.result.append((0, test, output, ''))
+        if self.verbosity > 1:
+            sys.stderr.write('ok ')
+            sys.stderr.write(str(test))
+            sys.stderr.write('\n')
+        else:
+            sys.stderr.write('.')
+
+    def addError(self, test, err):
+        TestResult.addError(self, test, err)
+        output = self.complete_output()
+        self.result.append((2, test, output, self._exc_info_to_string(err, test)))
+        if self.verbosity > 1:
+            sys.stderr.write('E  ')
+            sys.stderr.write(str(test))
+            sys.stderr.write('\n')
+        else:
+            sys.stderr.write('E')
+
+    def addFailure(self, test, err):
+        TestResult.addFailure(self, test, err)
+        output = self.complete_output()
+        self.result.append((1, test, output, self._exc_info_to_string(err, test)))
+        if self.verbosity > 1:
+            sys.stderr.write('F  ')
+            sys.stderr.write(str(test))
+            sys.stderr.write('\n')
+        else:
+            sys.stderr.write('F')
+
+
+class HTMLTestRunner:
+    """
+    """
+    def __init__(self, stream=sys.stdout, descriptions=1, verbosity=1, description='Test'):
+        # unittest itself has no good mechanism for user to define a
+        # description neither in TestCase nor TestSuite. Allow user to
+        # pass in the description as a parameter.
+
+        # note: this is different from unittest.TextTestRunner's
+        # 'descrpitions' parameter, which is an integer flag.
+
+        self.stream = stream
+        self.startTime = datetime.datetime.now()
+        self.description = description
+        self.verbosity = verbosity
+
+    def run(self, test):
+        "Run the given test case or test suite."
+        result = _TestResult(self.verbosity)
+        test(result)
+        self.stopTime = datetime.datetime.now()
+        self.generateReport(test, result)
+        print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
+        return result
+
+    def sortResult(self, result_list):
+        # unittest does not seems to run in any particular order.
+        # Here at least we want to group them together by class.
+        rmap = {}
+        classes = []
+        for n,t,o,e in result_list:
+            cls = t.__class__
+            if not rmap.has_key(cls):
+                rmap[cls] = []
+                classes.append(cls)
+            rmap[cls].append((n,t,o,e))
+        r = [(cls, rmap[cls]) for cls in classes]
+        return r
+
+    def generateReport(self, test, result):
+        rows = []
+        npAll = nfAll = neAll = 0
+        sortedResult = self.sortResult(result.result)
+        for cid, (cls, cls_results) in enumerate(sortedResult):
+            # update counts
+            np = nf = ne = 0
+            for n,t,o,e in cls_results:
+                if n == 0: np += 1
+                elif n == 1: nf += 1
+                else: ne += 1
+            npAll += np
+            nfAll += nf
+            neAll += ne
+
+            row = CLASS_TMPL.safe_substitute(
+                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
+                name = "%s.%s" % (cls.__module__, cls.__name__),
+                count = np+nf+ne,
+                Pass = np,
+                fail = nf,
+                error = ne,
+                cid = 'c%s' % (cid+1),
+            )
+            rows.append(row)
+
+            for tid, (n,t,o,e) in enumerate(cls_results):
+                # e.g. 'pt1.1', 'ft1.1', etc
+                has_output = bool(o or e)
+                tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
+                name = t.id().split('.')[-1]
+                tmpl = has_output and TEST_TMPL or TEST_TMPL_NO_OUTPUT
+                row = tmpl.safe_substitute(
+                    tid = tid,
+                    Class = (n == 0 and 'hiddenRow' or ''),
+                    style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or ''),
+                    name = name,
+                    status = STATUS[n],
+                )
+                rows.append(row)
+                if has_output:
+                    # o and e should be byte string because they are collected from stdout and stderr?
+                    if isinstance(o,str):
+# TODO: some problem with 'string_escape': it escape \n and mess up formating
+#                        uo = unicode(o.encode('string_escape'))
+                        uo = o.decode('latin-1')
+                    else:
+                        uo = o
+                    if isinstance(e,str):
+# TODO: some problem with 'string_escape': it escape \n and mess up formating
+#                        ue = unicode(e.encode('string_escape'))
+                        ue = e.decode('latin-1')
+                    else:
+                        ue = e
+                    row = TEST_OUTPUT_TMPL.safe_substitute(
+                        id = tid,
+                        output = saxutils.escape(uo+ue) \
+                            .replace("'", '&apos;') \
+                            .replace('"', '&quot;') \
+                            .replace('\\','\\\\') \
+                            .replace('\r','\\r') \
+                            .replace('\n','\\n'),
+                    )
+                    rows.append(row)
+
+        report = HTML_TMPL.safe_substitute(
+            title = self.description,
+            css = CSS,
+            description = self.description,
+            time = str(self.startTime)[:19],
+            status = result.wasSuccessful() and 'Passed' or 'Failed',
+            tests = ''.join(rows),
+            count = str(npAll+nfAll+neAll),
+            Pass = str(npAll),
+            fail = str(nfAll),
+            error = str(neAll),
+        )
+        self.stream.write(report.encode('utf8'))
+
+
+##############################################################################
+# Facilities for running tests from the command line
+##############################################################################
+
+# Note: Reuse unittest.TestProgram to launch test. In the future we may
+# build our own launcher to support more specific command line
+# parameters like test title, CSS, etc.
+class TestProgram(unittest.TestProgram):
+    """
+    A variation of the unittest.TestProgram. Please refer to the base
+    class for command line parameters.
+    """
+    def runTests(self):
+        # Pick HTMLTestRunner as the default test runner.
+        # base class's testRunner parameter is not useful because it means
+        # we have to instantiate HTMLTestRunner before we know self.verbosity.
+        if self.testRunner is None:
+            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
+        unittest.TestProgram.runTests(self)
+
+main = TestProgram
+
+##############################################################################
+# Executing this module from the command line
+##############################################################################
+
+if __name__ == "__main__":
+    main(module=None)
index de323f1e0eda6f02b3aca3e00bf49e9a926c9254..36e548ad0bf5a4d429958f7773d9dd14b121aef8 100644 (file)
@@ -1,3 +1,21 @@
+"""
+This program executes all unitest tests that are found in 
+    - directories with name test* or Test*
+    - files with name test* or Test*
+
+unitest tests are :
+    - functions and class with names test* or Test*
+    - methods with name test* or Test* from classes with name test* or Test*
+
+Typical uses are :
+   
+    - execute all tests with text output : python2.4 run.py 
+    - execute all tests with html output : python2.4 run.py --html
+    - execute some tests with text output : python2.4 run.py testelem
+    - execute one test with text output : python2.4 run.py testelem/testsimp1.py
+    - execute all tests with verbosity and html output : python2.4 run.py -v --html
+"""
+
 import sys,types,os
 import sre
 import unittest
@@ -142,13 +160,18 @@ class ModuleTestSuite(TestSuite):
           for test in func_tests ]
         return tests
 
+
 class TestProgram(unittest.TestProgram):
     USAGE="""
 """
-    def __init__(self,testRunner=None):
-        self.testRunner = testRunner
+    def __init__(self):
+        self.testRunner = None
         self.verbosity = 1
+        self.html=0
         self.parseArgs(sys.argv)
+        if self.html:
+           import HTMLTestRunner
+           self.testRunner = HTMLTestRunner.HTMLTestRunner(verbosity=self.verbosity)
         self.createTests()
         self.runTests()
 
@@ -157,9 +180,13 @@ class TestProgram(unittest.TestProgram):
         parser.add_option("-v","--verbose",action="count",
                           dest="verbosity",default=1,
                           help="Be more verbose. ")
+        parser.add_option("--html",action="store_true",
+                          dest="html",default=0,
+                          help="Produce HTML output ")
 
         options, args = parser.parse_args(argv)
         self.verbosity = options.verbosity
+        self.html=options.html
 
         if args:
             self.names = list(args)
@@ -169,9 +196,6 @@ class TestProgram(unittest.TestProgram):
     def createTests(self):
         self.test = TestSuite(self.names)
 
-
-main = TestProgram
-
 if __name__ == "__main__":
-    main()
+    TestProgram()