summaryrefslogtreecommitdiffstats
path: root/tests/test_ext_doctest.py
blob: c83e582aec967470f49258a055553527cb178f0d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""Test the doctest extension."""
import os
from collections import Counter

import pytest
from docutils import nodes
from packaging.specifiers import InvalidSpecifier
from packaging.version import InvalidVersion

from sphinx.ext.doctest import is_allowed_version

cleanup_called = 0


@pytest.mark.sphinx('doctest', testroot='ext-doctest')
def test_build(app, status, warning):
    global cleanup_called
    cleanup_called = 0
    app.builder.build_all()
    if app.statuscode != 0:
        raise AssertionError('failures in doctests:' + status.getvalue())
    # 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'


@pytest.mark.sphinx('dummy', testroot='ext-doctest')
def test_highlight_language_default(app, status, warning):
    app.build()
    doctree = app.env.get_doctree('doctest')
    for node in doctree.findall(nodes.literal_block):
        assert node['language'] in {'python', 'pycon', 'none'}


@pytest.mark.sphinx('dummy', testroot='ext-doctest',
                    confoverrides={'highlight_language': 'python'})
def test_highlight_language_python3(app, status, warning):
    app.build()
    doctree = app.env.get_doctree('doctest')
    for node in doctree.findall(nodes.literal_block):
        assert node['language'] in {'python', 'pycon', 'none'}


def test_is_allowed_version():
    assert is_allowed_version('<3.4', '3.3') is True
    assert is_allowed_version('<3.4', '3.3') is True
    assert is_allowed_version('<3.2', '3.3') is False
    assert is_allowed_version('<=3.4', '3.3') is True
    assert is_allowed_version('<=3.2', '3.3') is False
    assert is_allowed_version('==3.3', '3.3') is True
    assert is_allowed_version('==3.4', '3.3') is False
    assert is_allowed_version('>=3.2', '3.3') is True
    assert is_allowed_version('>=3.4', '3.3') is False
    assert is_allowed_version('>3.2', '3.3') is True
    assert is_allowed_version('>3.4', '3.3') is False
    assert is_allowed_version('~=3.4', '3.4.5') is True
    assert is_allowed_version('~=3.4', '3.5.0') is True

    # invalid spec
    with pytest.raises(InvalidSpecifier):
        is_allowed_version('&3.4', '3.5')

    # invalid version
    with pytest.raises(InvalidVersion):
        is_allowed_version('>3.4', 'Sphinx')


def cleanup_call():
    global cleanup_called
    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:
        raise AssertionError('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):
    recorded_calls[(directive, part, should_skip)] += 1
    return f'Recorded {directive} {part} {should_skip}'


@pytest.mark.sphinx('doctest', testroot='ext-doctest-with-autodoc')
def test_reporting_with_autodoc(app, status, warning, capfd):
    # Patch builder to get a copy of the output
    written = []
    app.builder._warn_out = written.append
    app.builder.build_all()

    failures = [line.replace(os.sep, '/')
                for line in '\n'.join(written).splitlines()
                if line.startswith('File')]

    assert 'File "dir/inner.rst", line 1, in default' in failures
    assert 'File "dir/bar.py", line ?, in default' in failures
    assert 'File "foo.py", line ?, in default' in failures
    assert 'File "index.rst", line 4, in default' in failures