diff options
Diffstat (limited to 'QMTest/unittest.py')
-rw-r--r-- | QMTest/unittest.py | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/QMTest/unittest.py b/QMTest/unittest.py new file mode 100644 index 0000000..f0540a5 --- /dev/null +++ b/QMTest/unittest.py @@ -0,0 +1,693 @@ +#!/usr/bin/env python +""" +Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's +Smalltalk testing framework. + +Further information is available in the bundled documentation, and from + + http://pyunit.sourceforge.net/ + +This module contains the core framework classes that form the basis of +specific test cases and suites (TestCase, TestSuite etc.), and also a +text-based utility class for running the tests and reporting the results +(TextTestRunner). + +Copyright (c) 1999, 2000, 2001 Steve Purcell +This module is free software, and you may redistribute it and/or modify +it under the same terms as Python itself, so long as this copyright message +and disclaimer are retained in their original form. + +IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +""" + +__author__ = "Steve Purcell (stephen_purcell@yahoo.com)" +__version__ = "$ Revision: 1.23 $"[11:-2] + +import time +import sys +import traceback +import string +import os + +############################################################################## +# A platform-specific concession to help the code work for JPython users +############################################################################## + +plat = string.lower(sys.platform) +_isJPython = string.find(plat, 'java') >= 0 or string.find(plat, 'jdk') >= 0 +del plat + + +############################################################################## +# Test framework core +############################################################################## + +class TestResult: + """Holder for test result information. + + Test results are automatically managed by the TestCase and TestSuite + classes, and do not need to be explicitly manipulated by writers of tests. + + Each instance holds the total number of tests run, and collections of + failures and errors that occurred among those test runs. The collections + contain tuples of (testcase, exceptioninfo), where exceptioninfo is a + tuple of values as returned by sys.exc_info(). + """ + def __init__(self): + self.failures = [] + self.errors = [] + self.testsRun = 0 + self.shouldStop = 0 + + def startTest(self, test): + "Called when the given test is about to be run" + self.testsRun = self.testsRun + 1 + + def stopTest(self, test): + "Called when the given test has been run" + pass + + def addError(self, test, err): + "Called when an error has occurred" + self.errors.append((test, err)) + + def addFailure(self, test, err): + "Called when a failure has occurred" + self.failures.append((test, err)) + + def wasSuccessful(self): + "Tells whether or not this result was a success" + return len(self.failures) == len(self.errors) == 0 + + def stop(self): + "Indicates that the tests should be aborted" + self.shouldStop = 1 + + def __repr__(self): + return "<%s run=%i errors=%i failures=%i>" % \ + (self.__class__, self.testsRun, len(self.errors), + len(self.failures)) + + +class TestCase: + """A class whose instances are single test cases. + + Test authors should subclass TestCase for their own tests. Construction + and deconstruction of the test's environment ('fixture') can be + implemented by overriding the 'setUp' and 'tearDown' methods respectively. + + By default, the test code itself should be placed in a method named + 'runTest'. + + If the fixture may be used for many test cases, create as + many test methods as are needed. When instantiating such a TestCase + subclass, specify in the constructor arguments the name of the test method + that the instance is to execute. + + If it is necessary to override the __init__ method, the base class + __init__ method must always be called. + """ + def __init__(self, methodName='runTest'): + """Create an instance of the class that will use the named test + method when executed. Raises a ValueError if the instance does + not have a method with the specified name. + """ + try: + self.__testMethod = getattr(self,methodName) + except AttributeError: + raise ValueError, "no such test method in %s: %s" % \ + (self.__class__, methodName) + + def setUp(self): + "Hook method for setting up the test fixture before exercising it." + pass + + def tearDown(self): + "Hook method for deconstructing the test fixture after testing it." + pass + + def countTestCases(self): + return 1 + + def defaultTestResult(self): + return TestResult() + + def shortDescription(self): + """Returns a one-line description of the test, or None if no + description has been provided. + + The default implementation of this method returns the first line of + the specified test method's docstring. + """ + doc = self.__testMethod.__doc__ + return doc and string.strip(string.split(doc, "\n")[0]) or None + + def id(self): + return "%s.%s" % (self.__class__, self.__testMethod.__name__) + + def __str__(self): + return "%s (%s)" % (self.__testMethod.__name__, self.__class__) + + def __repr__(self): + return "<%s testMethod=%s>" % \ + (self.__class__, self.__testMethod.__name__) + + def run(self, result=None): + return self(result) + + def __call__(self, result=None): + if result is None: result = self.defaultTestResult() + result.startTest(self) + try: + try: + self.setUp() + except: + result.addError(self,self.__exc_info()) + return + + try: + self.__testMethod() + except AssertionError, e: + result.addFailure(self,self.__exc_info()) + except: + result.addError(self,self.__exc_info()) + + try: + self.tearDown() + except: + result.addError(self,self.__exc_info()) + finally: + result.stopTest(self) + + def debug(self): + """Run the test without collecting errors in a TestResult""" + self.setUp() + self.__testMethod() + self.tearDown() + + def assert_(self, expr, msg=None): + """Equivalent of built-in 'assert', but is not optimised out when + __debug__ is false. + """ + if not expr: + raise AssertionError, msg + + failUnless = assert_ + + def failIf(self, expr, msg=None): + "Fail the test if the expression is true." + apply(self.assert_,(not expr,msg)) + + def assertRaises(self, excClass, callableObj, *args, **kwargs): + """Assert that an exception of class excClass is thrown + by callableObj when invoked with arguments args and keyword + arguments kwargs. If a different type of exception is + thrown, it will not be caught, and the test case will be + deemed to have suffered an error, exactly as for an + unexpected exception. + """ + try: + apply(callableObj, args, kwargs) + except excClass: + return + else: + if hasattr(excClass,'__name__'): excName = excClass.__name__ + else: excName = str(excClass) + raise AssertionError, excName + + def fail(self, msg=None): + """Fail immediately, with the given message.""" + raise AssertionError, msg + + def __exc_info(self): + """Return a version of sys.exc_info() with the traceback frame + minimised; usually the top level of the traceback frame is not + needed. + """ + exctype, excvalue, tb = sys.exc_info() + newtb = tb.tb_next + if newtb is None: + return (exctype, excvalue, tb) + return (exctype, excvalue, newtb) + + +class TestSuite: + """A test suite is a composite test consisting of a number of TestCases. + + For use, create an instance of TestSuite, then add test case instances. + When all tests have been added, the suite can be passed to a test + runner, such as TextTestRunner. It will run the individual test cases + in the order in which they were added, aggregating the results. When + subclassing, do not forget to call the base class constructor. + """ + def __init__(self, tests=()): + self._tests = [] + self.addTests(tests) + + def __repr__(self): + return "<%s tests=%s>" % (self.__class__, self._tests) + + __str__ = __repr__ + + def countTestCases(self): + cases = 0 + for test in self._tests: + cases = cases + test.countTestCases() + return cases + + def addTest(self, test): + self._tests.append(test) + + def addTests(self, tests): + for test in tests: + self.addTest(test) + + def run(self, result): + return self(result) + + def __call__(self, result): + for test in self._tests: + if result.shouldStop: + break + test(result) + return result + + def debug(self): + """Run the tests without collecting errors in a TestResult""" + for test in self._tests: test.debug() + + +class FunctionTestCase(TestCase): + """A test case that wraps a test function. + + This is useful for slipping pre-existing test functions into the + PyUnit framework. Optionally, set-up and tidy-up functions can be + supplied. As with TestCase, the tidy-up ('tearDown') function will + always be called if the set-up ('setUp') function ran successfully. + """ + + def __init__(self, testFunc, setUp=None, tearDown=None, + description=None): + TestCase.__init__(self) + self.__setUpFunc = setUp + self.__tearDownFunc = tearDown + self.__testFunc = testFunc + self.__description = description + + def setUp(self): + if self.__setUpFunc is not None: + self.__setUpFunc() + + def tearDown(self): + if self.__tearDownFunc is not None: + self.__tearDownFunc() + + def runTest(self): + self.__testFunc() + + def id(self): + return self.__testFunc.__name__ + + def __str__(self): + return "%s (%s)" % (self.__class__, self.__testFunc.__name__) + + def __repr__(self): + return "<%s testFunc=%s>" % (self.__class__, self.__testFunc) + + def shortDescription(self): + if self.__description is not None: return self.__description + doc = self.__testFunc.__doc__ + return doc and string.strip(string.split(doc, "\n")[0]) or None + + + +############################################################################## +# Convenience functions +############################################################################## + +def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp): + """Extracts all the names of functions in the given test case class + and its base classes that start with the given prefix. This is used + by makeSuite(). + """ + testFnNames = filter(lambda n,p=prefix: n[:len(p)] == p, + dir(testCaseClass)) + for baseclass in testCaseClass.__bases__: + testFnNames = testFnNames + \ + getTestCaseNames(baseclass, prefix, sortUsing=None) + if sortUsing: + testFnNames.sort(sortUsing) + return testFnNames + + +def makeSuite(testCaseClass, prefix='test', sortUsing=cmp): + """Returns a TestSuite instance built from all of the test functions + in the given test case class whose names begin with the given + prefix. The cases are sorted by their function names + using the supplied comparison function, which defaults to 'cmp'. + """ + cases = map(testCaseClass, + getTestCaseNames(testCaseClass, prefix, sortUsing)) + return TestSuite(cases) + + +def createTestInstance(name, module=None): + """Finds tests by their name, optionally only within the given module. + + Return the newly-constructed test, ready to run. If the name contains a ':' + then the portion of the name after the colon is used to find a specific + test case within the test case class named before the colon. + + Examples: + findTest('examples.listtests.suite') + -- returns result of calling 'suite' + findTest('examples.listtests.ListTestCase:checkAppend') + -- returns result of calling ListTestCase('checkAppend') + findTest('examples.listtests.ListTestCase:check-') + -- returns result of calling makeSuite(ListTestCase, prefix="check") + """ + + spec = string.split(name, ':') + if len(spec) > 2: raise ValueError, "illegal test name: %s" % name + if len(spec) == 1: + testName = spec[0] + caseName = None + else: + testName, caseName = spec + parts = string.split(testName, '.') + if module is None: + if len(parts) < 2: + raise ValueError, "incomplete test name: %s" % name + constructor = __import__(string.join(parts[:-1],'.')) + parts = parts[1:] + else: + constructor = module + for part in parts: + constructor = getattr(constructor, part) + if not callable(constructor): + raise ValueError, "%s is not a callable object" % constructor + if caseName: + if caseName[-1] == '-': + prefix = caseName[:-1] + if not prefix: + raise ValueError, "prefix too short: %s" % name + test = makeSuite(constructor, prefix=prefix) + else: + test = constructor(caseName) + else: + test = constructor() + if not hasattr(test,"countTestCases"): + raise TypeError, \ + "object %s found with spec %s is not a test" % (test, name) + return test + + +############################################################################## +# Text UI +############################################################################## + +class _WritelnDecorator: + """Used to decorate file-like objects with a handy 'writeln' method""" + def __init__(self,stream): + self.stream = stream + if _isJPython: + import java.lang.System + self.linesep = java.lang.System.getProperty("line.separator") + else: + self.linesep = os.linesep + + def __getattr__(self, attr): + return getattr(self.stream,attr) + + def writeln(self, *args): + if args: apply(self.write, args) + self.write(self.linesep) + + +class _JUnitTextTestResult(TestResult): + """A test result class that can print formatted text results to a stream. + + Used by JUnitTextTestRunner. + """ + def __init__(self, stream): + self.stream = stream + TestResult.__init__(self) + + def addError(self, test, error): + TestResult.addError(self,test,error) + self.stream.write('E') + self.stream.flush() + if error[0] is KeyboardInterrupt: + self.shouldStop = 1 + + def addFailure(self, test, error): + TestResult.addFailure(self,test,error) + self.stream.write('F') + self.stream.flush() + + def startTest(self, test): + TestResult.startTest(self,test) + self.stream.write('.') + self.stream.flush() + + def printNumberedErrors(self,errFlavour,errors): + if not errors: return + if len(errors) == 1: + self.stream.writeln("There was 1 %s:" % errFlavour) + else: + self.stream.writeln("There were %i %ss:" % + (len(errors), errFlavour)) + i = 1 + for test,error in errors: + errString = string.join(apply(traceback.format_exception,error),"") + self.stream.writeln("%i) %s" % (i, test)) + self.stream.writeln(errString) + i = i + 1 + + def printErrors(self): + self.printNumberedErrors("error",self.errors) + + def printFailures(self): + self.printNumberedErrors("failure",self.failures) + + def printHeader(self): + self.stream.writeln() + if self.wasSuccessful(): + self.stream.writeln("OK (%i tests)" % self.testsRun) + else: + self.stream.writeln("!!!FAILURES!!!") + self.stream.writeln("Test Results") + self.stream.writeln() + self.stream.writeln("Run: %i ; Failures: %i ; Errors: %i" % + (self.testsRun, len(self.failures), + len(self.errors))) + + def printResult(self): + self.printHeader() + self.printErrors() + self.printFailures() + + +class JUnitTextTestRunner: + """A test runner class that displays results in textual form. + + The display format approximates that of JUnit's 'textui' test runner. + This test runner may be removed in a future version of PyUnit. + """ + def __init__(self, stream=sys.stderr): + self.stream = _WritelnDecorator(stream) + + def run(self, test): + "Run the given test case or test suite." + result = _JUnitTextTestResult(self.stream) + startTime = time.time() + test(result) + stopTime = time.time() + self.stream.writeln() + self.stream.writeln("Time: %.3fs" % float(stopTime - startTime)) + result.printResult() + return result + + +############################################################################## +# Verbose text UI +############################################################################## + +class _VerboseTextTestResult(TestResult): + """A test result class that can print formatted text results to a stream. + + Used by VerboseTextTestRunner. + """ + def __init__(self, stream, descriptions): + TestResult.__init__(self) + self.stream = stream + self.lastFailure = None + self.descriptions = descriptions + + def startTest(self, test): + TestResult.startTest(self, test) + if self.descriptions: + self.stream.write(test.shortDescription() or str(test)) + else: + self.stream.write(str(test)) + self.stream.write(" ... ") + + def stopTest(self, test): + TestResult.stopTest(self, test) + if self.lastFailure is not test: + self.stream.writeln("ok") + + def addError(self, test, err): + TestResult.addError(self, test, err) + self._printError("ERROR", test, err) + self.lastFailure = test + if err[0] is KeyboardInterrupt: + self.shouldStop = 1 + + def addFailure(self, test, err): + TestResult.addFailure(self, test, err) + self._printError("FAIL", test, err) + self.lastFailure = test + + def _printError(self, flavour, test, err): + errLines = [] + separator1 = "\t" + '=' * 70 + separator2 = "\t" + '-' * 70 + if not self.lastFailure is test: + self.stream.writeln() + self.stream.writeln(separator1) + self.stream.writeln("\t%s" % flavour) + self.stream.writeln(separator2) + for line in apply(traceback.format_exception, err): + for l in string.split(line,"\n")[:-1]: + self.stream.writeln("\t%s" % l) + self.stream.writeln(separator1) + + +class VerboseTextTestRunner: + """A test runner class that displays results in textual form. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + def __init__(self, stream=sys.stderr, descriptions=1): + self.stream = _WritelnDecorator(stream) + self.descriptions = descriptions + + def run(self, test): + "Run the given test case or test suite." + result = _VerboseTextTestResult(self.stream, self.descriptions) + startTime = time.time() + test(result) + stopTime = time.time() + timeTaken = float(stopTime - startTime) + self.stream.writeln("-" * 78) + run = result.testsRun + self.stream.writeln("Ran %d test%s in %.3fs" % + (run, run > 1 and "s" or "", timeTaken)) + self.stream.writeln() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = map(len, (result.failures, result.errors)) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + else: + self.stream.writeln("OK") + return result + + +# Which flavour of TextTestRunner is the default? +TextTestRunner = VerboseTextTestRunner + + +############################################################################## +# Facilities for running tests from the command line +############################################################################## + +class TestProgram: + """A command-line program that runs a set of tests; this is primarily + for making test modules conveniently executable. + """ + USAGE = """\ +Usage: %(progName)s [-h|--help] [test[:(casename|prefix-)]] [...] + +Examples: + %(progName)s - run default set of tests + %(progName)s MyTestSuite - run suite 'MyTestSuite' + %(progName)s MyTestCase:checkSomething - run MyTestCase.checkSomething + %(progName)s MyTestCase:check- - run all 'check*' test methods + in MyTestCase +""" + def __init__(self, module='__main__', defaultTest=None, + argv=None, testRunner=None): + if type(module) == type(''): + self.module = __import__(module) + for part in string.split(module,'.')[1:]: + self.module = getattr(self.module, part) + else: + self.module = module + if argv is None: + argv = sys.argv + self.defaultTest = defaultTest + self.testRunner = testRunner + self.progName = os.path.basename(argv[0]) + self.parseArgs(argv) + self.createTests() + self.runTests() + + def usageExit(self, msg=None): + if msg: print msg + print self.USAGE % self.__dict__ + sys.exit(2) + + def parseArgs(self, argv): + import getopt + try: + options, args = getopt.getopt(argv[1:], 'hH', ['help']) + opts = {} + for opt, value in options: + if opt in ('-h','-H','--help'): + self.usageExit() + if len(args) == 0 and self.defaultTest is None: + raise getopt.error, "No default test is defined." + if len(args) > 0: + self.testNames = args + else: + self.testNames = (self.defaultTest,) + except getopt.error, msg: + self.usageExit(msg) + + def createTests(self): + tests = [] + for testName in self.testNames: + tests.append(createTestInstance(testName, self.module)) + self.test = TestSuite(tests) + + def runTests(self): + if self.testRunner is None: + self.testRunner = TextTestRunner() + result = self.testRunner.run(self.test) + sys.exit(not result.wasSuccessful()) + +main = TestProgram + + +############################################################################## +# Executing this module from the command line +############################################################################## + +if __name__ == "__main__": + main(module=None) |