#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 * #559: :confval:`html_add_permalinks` is now a string giving the
text to display in permalinks. text to display in permalinks.
* #553: Added :rst:dir:`testcleanup` blocks in the doctest extension.
Release 1.0.6 (in development) 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. 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] .. rst:directive:: .. doctest:: [group]
A doctest-style code block. You can use standard :mod:`doctest` flags for 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 .. 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 .. confval:: doctest_test_doctest_blocks
If this is a nonempty string (the default is ``'default'``), standard reST If this is a nonempty string (the default is ``'default'``), standard reST

View File

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

View File

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

View File

@@ -15,10 +15,20 @@ import StringIO
from util import * from util import *
status = StringIO.StringIO() status = StringIO.StringIO()
cleanup_called = 0
@with_app(buildername='doctest', status=status) @with_app(buildername='doctest', status=status)
def test_build(app): def test_build(app):
global cleanup_called
cleanup_called = 0
app.builder.build_all() app.builder.build_all()
if app.statuscode != 0: if app.statuscode != 0:
print >>sys.stderr, status.getvalue() print >>sys.stderr, status.getvalue()
assert False, 'failures in doctests' 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