mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
#553: Added :rst:dir:testcleanup blocks in the doctest extension.
This commit is contained in:
2
CHANGES
2
CHANGES
@@ -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)
|
||||
==============================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -121,3 +121,9 @@ Special directives
|
||||
.. testoutput:: group2
|
||||
|
||||
16
|
||||
|
||||
|
||||
.. testcleanup:: *
|
||||
|
||||
import test_doctest
|
||||
test_doctest.cleanup_call()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user