mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #5307 from akaihola/5273_doctest_conditional_skip
Feature: skip doctests conditionally
This commit is contained in:
commit
28131df93e
@ -198,6 +198,81 @@ The following is an example for the usage of the directives. The test via
|
|||||||
This parrot wouldn't voom if you put 3000 volts through it!
|
This parrot wouldn't voom if you put 3000 volts through it!
|
||||||
|
|
||||||
|
|
||||||
|
Skipping tests conditionally
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
``skipif``, a string option, can be used to skip directives conditionally. This
|
||||||
|
may be useful e.g. when a different set of tests should be run depending on the
|
||||||
|
environment (hardware, network/VPN, optional dependencies or different versions
|
||||||
|
of dependencies). The ``skipif`` option is supported by all of the doctest
|
||||||
|
directives. Below are typical use cases for ``skipif`` when used for different
|
||||||
|
directives:
|
||||||
|
|
||||||
|
- :rst:dir:`testsetup` and :rst:dir:`testcleanup`
|
||||||
|
|
||||||
|
- conditionally skip test setup and/or cleanup
|
||||||
|
- customize setup/cleanup code per environment
|
||||||
|
|
||||||
|
- :rst:dir:`doctest`
|
||||||
|
|
||||||
|
- conditionally skip both a test and its output verification
|
||||||
|
|
||||||
|
- :rst:dir:`testcode`
|
||||||
|
|
||||||
|
- conditionally skip a test
|
||||||
|
- customize test code per environment
|
||||||
|
|
||||||
|
- :rst:dir:`testoutput`
|
||||||
|
|
||||||
|
- conditionally skip output assertion for a skipped test
|
||||||
|
- expect different output depending on the environment
|
||||||
|
|
||||||
|
The value of the ``skipif`` option is evaluated as a Python expression. If the
|
||||||
|
result is a true value, the directive is omitted from the test run just as if
|
||||||
|
it wasn't present in the file at all.
|
||||||
|
|
||||||
|
Instead of repeating an expression, the :confval:`doctest_global_setup`
|
||||||
|
configuration option can be used to assign it to a variable which can then be
|
||||||
|
used instead.
|
||||||
|
|
||||||
|
Here's an example which skips some tests if Pandas is not installed:
|
||||||
|
|
||||||
|
.. code-block:: py
|
||||||
|
:caption: conf.py
|
||||||
|
|
||||||
|
extensions = ['sphinx.ext.doctest']
|
||||||
|
doctest_global_setup = '''
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
except ImportError:
|
||||||
|
pd = None
|
||||||
|
'''
|
||||||
|
|
||||||
|
.. code-block:: rst
|
||||||
|
:caption: contents.rst
|
||||||
|
|
||||||
|
.. testsetup::
|
||||||
|
:skipif: pd is None
|
||||||
|
|
||||||
|
data = pd.Series([42])
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
:skipif: pd is None
|
||||||
|
|
||||||
|
>>> data.iloc[0]
|
||||||
|
42
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
:skipif: pd is None
|
||||||
|
|
||||||
|
print(data.iloc[-1])
|
||||||
|
|
||||||
|
.. testoutput::
|
||||||
|
:skipif: pd is None
|
||||||
|
|
||||||
|
42
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -90,6 +90,16 @@ class TestDirective(SphinxDirective):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# type: () -> List[nodes.Node]
|
# type: () -> List[nodes.Node]
|
||||||
|
if 'skipif' in self.options:
|
||||||
|
condition = self.options['skipif']
|
||||||
|
context = {} # type: Dict[str, Any]
|
||||||
|
if self.config.doctest_global_setup:
|
||||||
|
exec(self.config.doctest_global_setup, context)
|
||||||
|
should_skip = eval(condition, context)
|
||||||
|
if self.config.doctest_global_cleanup:
|
||||||
|
exec(self.config.doctest_global_cleanup, context)
|
||||||
|
if should_skip:
|
||||||
|
return []
|
||||||
# use ordinary docutils nodes for test code: they get special attributes
|
# use ordinary docutils nodes for test code: they get special attributes
|
||||||
# so that our builder recognizes them, and the other builders are happy.
|
# so that our builder recognizes them, and the other builders are happy.
|
||||||
code = '\n'.join(self.content)
|
code = '\n'.join(self.content)
|
||||||
@ -155,11 +165,11 @@ class TestDirective(SphinxDirective):
|
|||||||
|
|
||||||
|
|
||||||
class TestsetupDirective(TestDirective):
|
class TestsetupDirective(TestDirective):
|
||||||
option_spec = {} # type: Dict
|
option_spec = {'skipif': directives.unchanged_required} # type: Dict
|
||||||
|
|
||||||
|
|
||||||
class TestcleanupDirective(TestDirective):
|
class TestcleanupDirective(TestDirective):
|
||||||
option_spec = {} # type: Dict
|
option_spec = {'skipif': directives.unchanged_required} # type: Dict
|
||||||
|
|
||||||
|
|
||||||
class DoctestDirective(TestDirective):
|
class DoctestDirective(TestDirective):
|
||||||
@ -167,6 +177,7 @@ class DoctestDirective(TestDirective):
|
|||||||
'hide': directives.flag,
|
'hide': directives.flag,
|
||||||
'options': directives.unchanged,
|
'options': directives.unchanged,
|
||||||
'pyversion': directives.unchanged_required,
|
'pyversion': directives.unchanged_required,
|
||||||
|
'skipif': directives.unchanged_required,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -174,6 +185,7 @@ class TestcodeDirective(TestDirective):
|
|||||||
option_spec = {
|
option_spec = {
|
||||||
'hide': directives.flag,
|
'hide': directives.flag,
|
||||||
'pyversion': directives.unchanged_required,
|
'pyversion': directives.unchanged_required,
|
||||||
|
'skipif': directives.unchanged_required,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -182,6 +194,7 @@ class TestoutputDirective(TestDirective):
|
|||||||
'hide': directives.flag,
|
'hide': directives.flag,
|
||||||
'options': directives.unchanged,
|
'options': directives.unchanged,
|
||||||
'pyversion': directives.unchanged_required,
|
'pyversion': directives.unchanged_required,
|
||||||
|
'skipif': directives.unchanged_required,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
16
tests/roots/test-ext-doctest-skipif/conf.py
Normal file
16
tests/roots/test-ext-doctest-skipif/conf.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
extensions = ['sphinx.ext.doctest']
|
||||||
|
|
||||||
|
project = 'test project for the doctest :skipif: directive'
|
||||||
|
master_doc = 'skipif'
|
||||||
|
source_suffix = '.txt'
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
doctest_global_setup = '''
|
||||||
|
from test_ext_doctest import record
|
||||||
|
|
||||||
|
record('doctest_global_setup', 'body', True)
|
||||||
|
'''
|
||||||
|
|
||||||
|
doctest_global_cleanup = '''
|
||||||
|
record('doctest_global_cleanup', 'body', True)
|
||||||
|
'''
|
81
tests/roots/test-ext-doctest-skipif/skipif.txt
Normal file
81
tests/roots/test-ext-doctest-skipif/skipif.txt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
Testing the doctest extension's `:skipif:` option
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
testsetup
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. testsetup:: group-skipif
|
||||||
|
:skipif: record('testsetup', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
record('testsetup', 'body', True)
|
||||||
|
|
||||||
|
.. testsetup:: group-skipif
|
||||||
|
:skipif: record('testsetup', ':skipif:', False) == 'this will be False'
|
||||||
|
|
||||||
|
record('testsetup', 'body', False)
|
||||||
|
|
||||||
|
|
||||||
|
doctest
|
||||||
|
-------
|
||||||
|
.. doctest:: group-skipif
|
||||||
|
:skipif: record('doctest', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
>>> print(record('doctest', 'body', True))
|
||||||
|
The test is skipped, and this expected text is ignored
|
||||||
|
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
:skipif: record('doctest', ':skipif:', False) == 'this will be False'
|
||||||
|
|
||||||
|
>>> print(record('doctest', 'body', False))
|
||||||
|
Recorded doctest body False
|
||||||
|
|
||||||
|
|
||||||
|
testcode and testoutput
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
testcode skipped
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. testcode:: group-skipif
|
||||||
|
:skipif: record('testcode', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
print(record('testcode', 'body', True))
|
||||||
|
|
||||||
|
.. testoutput:: group-skipif
|
||||||
|
:skipif: record('testoutput-1', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
The previous testcode is skipped, and the :skipif: condition is True,
|
||||||
|
so this testoutput is ignored
|
||||||
|
|
||||||
|
testcode executed
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. testcode:: group-skipif
|
||||||
|
:skipif: record('testcode', ':skipif:', False) == 'this will be False'
|
||||||
|
|
||||||
|
print(record('testcode', 'body', False))
|
||||||
|
|
||||||
|
.. testoutput:: group-skipif
|
||||||
|
:skipif: record('testoutput-2', ':skipif:', False) == 'this will be False'
|
||||||
|
|
||||||
|
Recorded testcode body False
|
||||||
|
|
||||||
|
.. testoutput:: group-skipif
|
||||||
|
:skipif: record('testoutput-2', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
The :skipif: condition is False, so this testoutput is ignored
|
||||||
|
|
||||||
|
|
||||||
|
testcleanup
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. testcleanup:: group-skipif
|
||||||
|
:skipif: record('testcleanup', ':skipif:', True) != 'this will be True'
|
||||||
|
|
||||||
|
record('testcleanup', 'body', True)
|
||||||
|
|
||||||
|
.. testcleanup:: group-skipif
|
||||||
|
:skipif: record('testcleanup', ':skipif:', False) == 'this will be False'
|
||||||
|
|
||||||
|
record('testcleanup', 'body', False)
|
@ -9,6 +9,7 @@
|
|||||||
:license: BSD, see LICENSE for details.
|
:license: BSD, see LICENSE for details.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.specifiers import InvalidSpecifier
|
from packaging.specifiers import InvalidSpecifier
|
||||||
@ -61,6 +62,56 @@ def cleanup_call():
|
|||||||
cleanup_called += 1
|
cleanup_called += 1
|
||||||
|
|
||||||
|
|
||||||
|
recorded_calls = Counter()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.sphinx('doctest', testroot='ext-doctest-skipif')
|
||||||
|
def test_skipif(app, status, warning):
|
||||||
|
"""Tests for the :skipif: option
|
||||||
|
|
||||||
|
The tests are separated into a different test root directory since the
|
||||||
|
``app`` object only evaluates options once in its lifetime. If these tests
|
||||||
|
were combined with the other doctest tests, the ``:skipif:`` evaluations
|
||||||
|
would be recorded only on the first ``app.builder.build_all()`` run, i.e.
|
||||||
|
in ``test_build`` above, and the assertion below would fail.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global recorded_calls
|
||||||
|
recorded_calls = Counter()
|
||||||
|
app.builder.build_all()
|
||||||
|
if app.statuscode != 0:
|
||||||
|
assert False, 'failures in doctests:' + status.getvalue()
|
||||||
|
# The `:skipif:` expressions are always run.
|
||||||
|
# Actual tests and setup/cleanup code is only run if the `:skipif:`
|
||||||
|
# expression evaluates to a False value.
|
||||||
|
# Global setup/cleanup are run before/after evaluating the `:skipif:`
|
||||||
|
# option in each directive - thus 11 additional invocations for each on top
|
||||||
|
# of the ones made for the whole test file.
|
||||||
|
assert recorded_calls == {('doctest_global_setup', 'body', True): 13,
|
||||||
|
('testsetup', ':skipif:', True): 1,
|
||||||
|
('testsetup', ':skipif:', False): 1,
|
||||||
|
('testsetup', 'body', False): 1,
|
||||||
|
('doctest', ':skipif:', True): 1,
|
||||||
|
('doctest', ':skipif:', False): 1,
|
||||||
|
('doctest', 'body', False): 1,
|
||||||
|
('testcode', ':skipif:', True): 1,
|
||||||
|
('testcode', ':skipif:', False): 1,
|
||||||
|
('testcode', 'body', False): 1,
|
||||||
|
('testoutput-1', ':skipif:', True): 1,
|
||||||
|
('testoutput-2', ':skipif:', True): 1,
|
||||||
|
('testoutput-2', ':skipif:', False): 1,
|
||||||
|
('testcleanup', ':skipif:', True): 1,
|
||||||
|
('testcleanup', ':skipif:', False): 1,
|
||||||
|
('testcleanup', 'body', False): 1,
|
||||||
|
('doctest_global_cleanup', 'body', True): 13}
|
||||||
|
|
||||||
|
|
||||||
|
def record(directive, part, should_skip):
|
||||||
|
global recorded_calls
|
||||||
|
recorded_calls[(directive, part, should_skip)] += 1
|
||||||
|
return 'Recorded {} {} {}'.format(directive, part, should_skip)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
@pytest.mark.xfail(
|
||||||
PY2, reason='node.source points to document instead of filename',
|
PY2, reason='node.source points to document instead of filename',
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user