#553: Added :rst:dir:testcleanup blocks in the doctest extension.

This commit is contained in:
Georg Brandl
2011-01-03 21:20:29 +01:00
parent 6c7aa8bb6d
commit 7401622583
5 changed files with 91 additions and 21 deletions

View File

@@ -28,6 +28,8 @@ Release 1.1 (in development)
* #559: :confval:`html_add_permalinks` is now a string giving the
text to display in permalinks.
* #553: Added :rst:dir:`testcleanup` blocks in the doctest extension.
Release 1.0.6 (in development)
==============================

View File

@@ -45,6 +45,14 @@ names.
but executed before the doctests of the group(s) it belongs to.
.. rst:directive:: .. testcleanup:: [group]
A cleanup code block. This code is not shown in the output for other
builders, but executed after the doctests of the group(s) it belongs to.
.. versionadded:: 1.1
.. rst:directive:: .. doctest:: [group]
A doctest-style code block. You can use standard :mod:`doctest` flags for
@@ -181,6 +189,14 @@ There are also these config values for customizing the doctest extension:
.. versionadded:: 0.6
.. confval:: doctest_global_cleanup
Python code that is treated like it were put in a ``testcleanup`` directive
for *every* file that is tested, and for every group. You can use this to
e.g. remove any temporary files that the tests leave behind.
.. versionadded:: 1.1
.. confval:: doctest_test_doctest_blocks
If this is a nonempty string (the default is ``'default'``), standard reST

View File

@@ -56,7 +56,7 @@ class TestDirective(Directive):
test = code
code = doctestopt_re.sub('', code)
nodetype = nodes.literal_block
if self.name == 'testsetup' or 'hide' in self.options:
if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options:
nodetype = nodes.comment
if self.arguments:
groups = [x.strip() for x in self.arguments[0].split(',')]
@@ -86,6 +86,9 @@ class TestDirective(Directive):
class TestsetupDirective(TestDirective):
option_spec = {}
class TestcleanupDirective(TestDirective):
option_spec = {}
class DoctestDirective(TestDirective):
option_spec = {
'hide': directives.flag,
@@ -113,6 +116,7 @@ class TestGroup(object):
self.name = name
self.setup = []
self.tests = []
self.cleanup = []
def add_code(self, code, prepend=False):
if code.type == 'testsetup':
@@ -120,6 +124,8 @@ class TestGroup(object):
self.setup.insert(0, code)
else:
self.setup.append(code)
elif code.type == 'testcleanup':
self.cleanup.append(code)
elif code.type == 'doctest':
self.tests.append([code])
elif code.type == 'testcode':
@@ -131,8 +137,8 @@ class TestGroup(object):
raise RuntimeError('invalid TestCode type')
def __repr__(self):
return 'TestGroup(name=%r, setup=%r, tests=%r)' % (
self.name, self.setup, self.tests)
return 'TestGroup(name=%r, setup=%r, cleanup=%r, tests=%r)' % (
self.name, self.setup, self.cleanup, self.tests)
class TestCode(object):
@@ -204,6 +210,8 @@ class DocTestBuilder(Builder):
self.total_tries = 0
self.setup_failures = 0
self.setup_tries = 0
self.cleanup_failures = 0
self.cleanup_tries = 0
date = time.strftime('%Y-%m-%d %H:%M:%S')
@@ -240,12 +248,14 @@ Doctest summary
%5d test%s
%5d failure%s in tests
%5d failure%s in setup code
%5d failure%s in cleanup code
''' % (self.total_tries, s(self.total_tries),
self.total_failures, s(self.total_failures),
self.setup_failures, s(self.setup_failures)))
self.setup_failures, s(self.setup_failures),
self.cleanup_failures, s(self.cleanup_failures)))
self.outfile.close()
if self.total_failures or self.setup_failures:
if self.total_failures or self.setup_failures or self.cleanup_failures:
self.app.statuscode = 1
def write(self, build_docnames, updated_docnames, method='update'):
@@ -265,8 +275,11 @@ Doctest summary
optionflags=self.opt)
self.test_runner = SphinxDocTestRunner(verbose=False,
optionflags=self.opt)
self.cleanup_runner = SphinxDocTestRunner(verbose=False,
optionflags=self.opt)
self.test_runner._fakeout = self.setup_runner._fakeout
self.cleanup_runner._fakeout = self.setup_runner._fakeout
if self.config.doctest_test_doctest_blocks:
def condition(node):
@@ -301,6 +314,11 @@ Doctest summary
'testsetup', lineno=0)
for group in groups.itervalues():
group.add_code(code, prepend=True)
if self.config.doctest_global_cleanup:
code = TestCode(self.config.doctest_global_cleanup,
'testcleanup', lineno=0)
for group in groups.itervalues():
group.add_code(code)
if not groups:
return
@@ -316,29 +334,42 @@ Doctest summary
res_f, res_t = self.test_runner.summarize(self._out, verbose=True)
self.total_failures += res_f
self.total_tries += res_t
if self.cleanup_runner.tries:
res_f, res_t = self.cleanup_runner.summarize(self._out, verbose=True)
self.cleanup_failures += res_f
self.cleanup_tries += res_t
def compile(self, code, name, type, flags, dont_inherit):
return compile(code, name, self.type, flags, dont_inherit)
def test_group(self, group, filename):
ns = {}
setup_examples = []
for setup in group.setup:
setup_examples.append(doctest.Example(setup.code, '',
lineno=setup.lineno))
if setup_examples:
# simulate a doctest with the setup code
setup_doctest = doctest.DocTest(setup_examples, {},
'%s (setup code)' % group.name,
filename, 0, None)
setup_doctest.globs = ns
old_f = self.setup_runner.failures
self.type = 'exec' # the snippet may contain multiple statements
self.setup_runner.run(setup_doctest, out=self._warn_out,
clear_globs=False)
if self.setup_runner.failures > old_f:
# don't run the group
def run_setup_cleanup(runner, testcodes, what):
examples = []
for testcode in testcodes:
examples.append(doctest.Example(testcode.code, '',
lineno=testcode.lineno))
if not examples:
return
# simulate a doctest with the code
sim_doctest = doctest.DocTest(examples, {},
'%s (%s code)' % (group.name, what),
filename, 0, None)
sim_doctest.globs = ns
old_f = runner.failures
self.type = 'exec' # the snippet may contain multiple statements
runner.run(sim_doctest, out=self._warn_out, clear_globs=False)
if runner.failures > old_f:
return False
return True
# run the setup code
if not run_setup_cleanup(self.setup_runner, group.setup, 'setup'):
# if setup failed, don't run the group
return
# run the tests
for code in group.tests:
if len(code) == 1:
# ordinary doctests (code/output interleaved)
@@ -376,9 +407,13 @@ Doctest summary
# also don't clear the globs namespace after running the doctest
self.test_runner.run(test, out=self._warn_out, clear_globs=False)
# run the cleanup
run_setup_cleanup(self.cleanup_runner, group.cleanup, 'cleanup')
def setup(app):
app.add_directive('testsetup', TestsetupDirective)
app.add_directive('testcleanup', TestcleanupDirective)
app.add_directive('doctest', DoctestDirective)
app.add_directive('testcode', TestcodeDirective)
app.add_directive('testoutput', TestoutputDirective)
@@ -387,3 +422,4 @@ def setup(app):
app.add_config_value('doctest_path', [], False)
app.add_config_value('doctest_test_doctest_blocks', 'default', False)
app.add_config_value('doctest_global_setup', '', False)
app.add_config_value('doctest_global_cleanup', '', False)

View File

@@ -121,3 +121,9 @@ Special directives
.. testoutput:: group2
16
.. testcleanup:: *
import test_doctest
test_doctest.cleanup_call()

View File

@@ -15,10 +15,20 @@ import StringIO
from util import *
status = StringIO.StringIO()
cleanup_called = 0
@with_app(buildername='doctest', status=status)
def test_build(app):
global cleanup_called
cleanup_called = 0
app.builder.build_all()
if app.statuscode != 0:
print >>sys.stderr, status.getvalue()
assert False, 'failures in doctests'
# in doctest.txt, there are two named groups and the default group,
# so the cleanup function must be called three times
assert cleanup_called == 3, 'testcleanup did not get executed enough times'
def cleanup_call():
global cleanup_called
cleanup_called += 1