Ported some features from zope:

- Fixed the display of tests in verbose output
- Allow setUp and tearDown functions to be provided for DocTestSuites.
This commit is contained in:
Jim Fulton 2004-07-14 19:06:50 +00:00
parent b91af521fd
commit a643b658a7

View file

@ -289,7 +289,6 @@ __all__ = [
'run_docstring_examples', 'run_docstring_examples',
'is_private', 'is_private',
'Tester', 'Tester',
'DocTestTestFailure',
'DocTestSuite', 'DocTestSuite',
'testsource', 'testsource',
'debug', 'debug',
@ -1289,66 +1288,92 @@ def _find_tests(module, prefix=None):
_extract_doctests(mdict.items(), module, mdict, tests, prefix) _extract_doctests(mdict.items(), module, mdict, tests, prefix)
return tests return tests
# unittest helpers. ###############################################################################
# unitest support
# A function passed to unittest, for unittest to drive. from StringIO import StringIO
# tester is doctest Tester instance. doc is the docstring whose import os
# doctests are to be run. import sys
import tempfile
import unittest
def _utest(tester, name, doc, filename, lineno): class DocTestTestCase(unittest.TestCase):
import sys """A test case that wraps a test function.
from StringIO import StringIO
old = sys.stdout This is useful for slipping pre-existing test functions into the
sys.stdout = new = StringIO() PyUnit framework. Optionally, set-up and tidy-up functions can be
try: supplied. As with TestCase, the tidy-up ('tearDown') function will
failures, tries = tester.runstring(doc, name) always be called if the set-up ('setUp') function ran successfully.
finally:
sys.stdout = old
if failures:
msg = new.getvalue()
lname = '.'.join(name.split('.')[-1:])
if not lineno:
lineno = "0 (don't know line number)"
# Don't change this format! It was designed so that Emacs can
# parse it naturally.
raise DocTestTestFailure('Failed doctest test for %s\n'
' File "%s", line %s, in %s\n\n%s' %
(name, filename, lineno, lname, msg))
class DocTestTestFailure(Exception):
"""A doctest test failed"""
def DocTestSuite(module=None):
"""Convert doctest tests for a module to a unittest TestSuite.
The returned TestSuite is to be run by the unittest framework, and
runs each doctest in the module. If any of the doctests fail,
then the synthesized unit test fails, and an error is raised showing
the name of the file containing the test and a (sometimes approximate)
line number.
The optional module argument provides the module to be tested. It
can be a module object or a (possibly dotted) module name. If not
specified, the module calling DocTestSuite() is used.
Example (although note that unittest supplies many ways to use the
TestSuite returned; see the unittest docs):
import unittest
import doctest
import my_module_with_doctests
suite = doctest.DocTestSuite(my_module_with_doctests)
runner = unittest.TextTestRunner()
runner.run(suite)
""" """
import unittest def __init__(self, tester, name, doc, filename, lineno,
setUp=None, tearDown=None):
unittest.TestCase.__init__(self)
(self.__tester, self.__name, self.__doc,
self.__filename, self.__lineno,
self.__setUp, self.__tearDown
) = tester, name, doc, filename, lineno, setUp, tearDown
def setUp(self):
if self.__setUp is not None:
self.__setUp()
def tearDown(self):
if self.__tearDown is not None:
self.__tearDown()
def runTest(self):
old = sys.stdout
new = StringIO()
try:
sys.stdout = new
failures, tries = self.__tester.runstring(self.__doc, self.__name)
finally:
sys.stdout = old
if failures:
lname = '.'.join(self.__name.split('.')[-1:])
lineno = self.__lineno or "0 (don't know line no)"
raise self.failureException(
'Failed doctest test for %s\n'
' File "%s", line %s, in %s\n\n%s'
% (self.__name, self.__filename, lineno, lname, new.getvalue())
)
def id(self):
return self.__name
def __repr__(self):
name = self.__name.split('.')
return "%s (%s)" % (name[-1], '.'.join(name[:-1]))
__str__ = __repr__
def shortDescription(self):
return "Doctest: " + self.__name
def DocTestSuite(module=None,
setUp=lambda: None,
tearDown=lambda: None,
):
"""Convert doctest tests for a mudule to a unittest test suite
This tests convers each documentation string in a module that
contains doctest tests to a unittest test case. If any of the
tests in a doc string fail, then the test case fails. An error is
raised showing the name of the file containing the test and a
(sometimes approximate) line number.
A module argument provides the module to be tested. The argument
can be either a module or a module name.
If no argument is given, the calling module is used.
"""
module = _normalizeModule(module)
tests = _findTests(module)
module = _normalize_module(module)
tests = _find_tests(module)
if not tests: if not tests:
raise ValueError(module, "has no tests") raise ValueError(module, "has no tests")
@ -1362,78 +1387,166 @@ def DocTestSuite(module=None):
filename = filename[:-1] filename = filename[:-1]
elif filename.endswith(".pyo"): elif filename.endswith(".pyo"):
filename = filename[:-1] filename = filename[:-1]
def runit(name=name, doc=doc, filename=filename, lineno=lineno):
_utest(tester, name, doc, filename, lineno) suite.addTest(DocTestTestCase(
suite.addTest(unittest.FunctionTestCase( tester, name, doc, filename, lineno,
runit, setUp, tearDown))
description="doctest of " + name))
return suite return suite
# Debugging support. def _normalizeModule(module):
# Normalize a module
if module is None:
# Test the calling module
module = sys._getframe(2).f_globals['__name__']
module = sys.modules[module]
elif isinstance(module, (str, unicode)):
module = __import__(module, globals(), locals(), ["*"])
return module
def _doc(name, object, tests, prefix, filename='', lineno=''):
doc = getattr(object, '__doc__', '')
if doc and doc.find('>>>') >= 0:
tests.append((prefix+name, doc, filename, lineno))
def _findTests(module, prefix=None):
if prefix is None:
prefix = module.__name__
dict = module.__dict__
tests = []
_doc(prefix, module, tests, '',
lineno="1 (or below)")
prefix = prefix and (prefix + ".")
_find(dict.items(), module, dict, tests, prefix)
return tests
def _find(items, module, dict, tests, prefix, minlineno=0):
for name, object in items:
# Only interested in named objects
if not hasattr(object, '__name__'):
continue
if hasattr(object, 'func_globals'):
# Looks like a func
if object.func_globals is not dict:
# Non-local func
continue
code = getattr(object, 'func_code', None)
filename = getattr(code, 'co_filename', '')
lineno = getattr(code, 'co_firstlineno', -1) + 1
if minlineno:
minlineno = min(lineno, minlineno)
else:
minlineno = lineno
_doc(name, object, tests, prefix, filename, lineno)
elif hasattr(object, "__module__"):
# Maybe a class-like things. In which case, we care
if object.__module__ != module.__name__:
continue # not the same module
if not (hasattr(object, '__dict__')
and hasattr(object, '__bases__')):
continue # not a class
lineno = _find(object.__dict__.items(), module, dict, tests,
prefix+name+".")
_doc(name, object, tests, prefix,
lineno="%s (or above)" % (lineno-3))
return minlineno
# end unitest support
###############################################################################
###############################################################################
# debugger
def _expect(expect): def _expect(expect):
# Return the expected output (if any), formatted as a Python # Return the expected output, if any
# comment block.
if expect: if expect:
expect = "\n# ".join(expect.split("\n")) expect = "\n# ".join(expect.split("\n"))
expect = "\n# Expect:\n# %s" % expect expect = "\n# Expect:\n# %s" % expect
return expect return expect
def testsource(module, name): def testsource(module, name):
"""Extract the doctest examples from a docstring. """Extract the test sources from a doctest test docstring as a script
Provide the module (or dotted name of the module) containing the Provide the module (or dotted name of the module) containing the
tests to be extracted, and the name (within the module) of the object test to be debugged and the name (within the module) of the object
with the docstring containing the tests to be extracted. with the doc string with tests to be debugged.
The doctest examples are returned as a string containing Python
code. The expected output blocks in the examples are converted
to Python comments.
""" """
module = _normalizeModule(module)
module = _normalize_module(module) tests = _findTests(module, "")
tests = _find_tests(module, "") test = [doc for (tname, doc, f, l) in tests if tname == name]
test = [doc for (tname, doc, dummy, dummy) in tests
if tname == name]
if not test: if not test:
raise ValueError(name, "not found in tests") raise ValueError(name, "not found in tests")
test = test[0] test = test[0]
examples = [source + _expect(expect) # XXX we rely on an internal doctest function:
for source, expect, dummy in _extract_examples(test)] examples = _extract_examples(test)
return '\n'.join(examples) testsrc = '\n'.join([
"%s%s" % (source, _expect(expect))
for (source, expect, lineno) in examples
])
return testsrc
def debug(module, name): def debug_src(src, pm=False, globs=None):
"""Debug a single docstring containing doctests. """Debug a single doctest test doc string
Provide the module (or dotted name of the module) containing the The string is provided directly
docstring to be debugged, and the name (within the module) of the
object with the docstring to be debugged.
The doctest examples are extracted (see function testsource()),
and written to a temp file. The Python debugger (pdb) is then
invoked on that file.
""" """
# XXX we rely on an internal doctest function:
examples = _extract_examples(src)
src = '\n'.join([
"%s%s" % (source, _expect(expect))
for (source, expect, lineno) in examples
])
debug_script(src, pm, globs)
import os def debug_script(src, pm=False, globs=None):
"Debug a test script"
import pdb import pdb
import tempfile
module = _normalize_module(module)
testsrc = testsource(module, name)
srcfilename = tempfile.mktemp("doctestdebug.py") srcfilename = tempfile.mktemp("doctestdebug.py")
f = file(srcfilename, 'w') open(srcfilename, 'w').write(src)
f.write(testsrc) if globs:
f.close() globs = globs.copy()
else:
globs = {}
globs = {}
globs.update(module.__dict__)
try: try:
# Note that %r is vital here. '%s' instead can, e.g., cause if pm:
# backslashes to get treated as metacharacters on Windows. try:
pdb.run("execfile(%r)" % srcfilename, globs, globs) execfile(srcfilename, globs, globs)
except:
print sys.exc_info()[1]
pdb.post_mortem(sys.exc_info()[2])
else:
# Note that %r is vital here. '%s' instead can, e.g., cause
# backslashes to get treated as metacharacters on Windows.
pdb.run("execfile(%r)" % srcfilename, globs, globs)
finally: finally:
os.remove(srcfilename) os.remove(srcfilename)
def debug(module, name, pm=False):
"""Debug a single doctest test doc string
Provide the module (or dotted name of the module) containing the
test to be debugged and the name (within the module) of the object
with the doc string with tests to be debugged.
"""
module = _normalizeModule(module)
testsrc = testsource(module, name)
debug_script(testsrc, pm, module.__dict__)
# end debugger
###############################################################################
class _TestClass: class _TestClass: