Commits

Georg Brandl committed 92e638d

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

  • Participants
  • Parent commits 7955f5a

Comments (0)

Files changed (5)

 * #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)
 ==============================

File doc/ext/doctest.rst

    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
 
    .. 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

File sphinx/ext/doctest.py

                     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(',')]
 class TestsetupDirective(TestDirective):
     option_spec = {}
 
+class TestcleanupDirective(TestDirective):
+    option_spec = {}
+
 class DoctestDirective(TestDirective):
     option_spec = {
         'hide': directives.flag,
         self.name = name
         self.setup = []
         self.tests = []
+        self.cleanup = []
 
     def add_code(self, code, prepend=False):
         if code.type == 'testsetup':
                 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':
             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):
         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')
 
 %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'):
                                                 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):
                             '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
 
             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
+
+        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
-            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
+            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)
             # 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)
     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)

File tests/root/doctest.txt

   .. testoutput:: group2
 
      16
+
+
+.. testcleanup:: *
+
+   import test_doctest
+   test_doctest.cleanup_call()

File tests/test_doctest.py

 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