From 767ea6d0f9f23957072ec6d3b855653afa37783a Mon Sep 17 00:00:00 2001 From: eficas <> Date: Sun, 26 Feb 2006 14:08:44 +0000 Subject: [PATCH] CCAR: ajout HTMLTestRunner --- Tests/HTMLTestRunner.py | 538 ++++++++++++++++++++++++++++++++++++++++ Tests/run.py | 36 ++- 2 files changed, 568 insertions(+), 6 deletions(-) create mode 100644 Tests/HTMLTestRunner.py diff --git a/Tests/HTMLTestRunner.py b/Tests/HTMLTestRunner.py new file mode 100644 index 00000000..e59a8efa --- /dev/null +++ b/Tests/HTMLTestRunner.py @@ -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 = """ + +""" + +# currently not used +CSS_LINK = '\n' + + +HTML_TMPL = string.Template(r""" + + + + $title + + $css + + + + +

$description

+

Time: $time

+

Status: $status

+

Show +Summary +Failed +All +

+ ++++++++ + + + + + + + + +$tests + + + + + + + + +
Class/Test caseCountPassFailErrorView
Total$count$Pass$fail$error 
+
+ + +""") + +CLASS_TMPL = string.Template(r""" + + $name + $count + $Pass + $fail + $error + Detail + +""") + +TEST_TMPL = string.Template(r""" + +
$name
+ $status + +""") + +TEST_TMPL_NO_OUTPUT = string.Template(r""" + +
$name
+ $status + +""") + +TEST_OUTPUT_TMPL = string.Template(r""" + +""") + + +# ---------------------------------------------------------------------- + +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("'", ''') \ + .replace('"', '"') \ + .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) diff --git a/Tests/run.py b/Tests/run.py index de323f1e..36e548ad 100644 --- a/Tests/run.py +++ b/Tests/run.py @@ -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() -- 2.39.2