summaryrefslogtreecommitdiffstats
path: root/tests/test_builders
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:59 +0000
commit5de84c9242643f786eff03726286578726d7d390 (patch)
tree8e8eadab2b786c41d7b8a2cdafbb467588928ad0 /tests/test_builders
parentReleasing progress-linux version 7.2.6-8~progress7.99u1. (diff)
downloadsphinx-5de84c9242643f786eff03726286578726d7d390.tar.xz
sphinx-5de84c9242643f786eff03726286578726d7d390.zip
Merging upstream version 7.3.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--tests/test_builders/__init__.py0
-rw-r--r--tests/test_builders/conftest.py28
-rw-r--r--tests/test_builders/test_build.py (renamed from tests/test_build.py)38
-rw-r--r--tests/test_builders/test_build_changes.py (renamed from tests/test_build_changes.py)2
-rw-r--r--tests/test_builders/test_build_dirhtml.py (renamed from tests/test_build_dirhtml.py)0
-rw-r--r--tests/test_builders/test_build_epub.py (renamed from tests/test_build_epub.py)38
-rw-r--r--tests/test_builders/test_build_gettext.py (renamed from tests/test_build_gettext.py)18
-rw-r--r--tests/test_builders/test_build_html.py378
-rw-r--r--tests/test_builders/test_build_html_5_output.py276
-rw-r--r--tests/test_builders/test_build_html_assets.py152
-rw-r--r--tests/test_builders/test_build_html_code.py46
-rw-r--r--tests/test_builders/test_build_html_download.py62
-rw-r--r--tests/test_builders/test_build_html_highlight.py61
-rw-r--r--tests/test_builders/test_build_html_image.py80
-rw-r--r--tests/test_builders/test_build_html_maths.py58
-rw-r--r--tests/test_builders/test_build_html_numfig.py487
-rw-r--r--tests/test_builders/test_build_html_tocdepth.py94
-rw-r--r--tests/test_builders/test_build_latex.py (renamed from tests/test_build_latex.py)274
-rw-r--r--tests/test_builders/test_build_linkcheck.py (renamed from tests/test_build_linkcheck.py)401
-rw-r--r--tests/test_builders/test_build_manpage.py (renamed from tests/test_build_manpage.py)7
-rw-r--r--tests/test_builders/test_build_texinfo.py (renamed from tests/test_build_texinfo.py)35
-rw-r--r--tests/test_builders/test_build_text.py (renamed from tests/test_build_text.py)28
-rw-r--r--tests/test_builders/test_build_warnings.py89
-rw-r--r--tests/test_builders/test_builder.py (renamed from tests/test_builder.py)5
-rw-r--r--tests/test_builders/xpath_data.py8
-rw-r--r--tests/test_builders/xpath_util.py79
26 files changed, 2356 insertions, 388 deletions
diff --git a/tests/test_builders/__init__.py b/tests/test_builders/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_builders/__init__.py
diff --git a/tests/test_builders/conftest.py b/tests/test_builders/conftest.py
new file mode 100644
index 0000000..1203d5d
--- /dev/null
+++ b/tests/test_builders/conftest.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from sphinx.testing.util import etree_parse
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+ from pathlib import Path
+ from xml.etree.ElementTree import ElementTree
+
+_etree_cache: dict[Path, ElementTree] = {}
+
+
+def _parse(path: Path) -> ElementTree:
+ if path in _etree_cache:
+ return _etree_cache[path]
+
+ _etree_cache[path] = tree = etree_parse(path)
+ return tree
+
+
+@pytest.fixture(scope='package')
+def cached_etree_parse() -> Iterator[Callable[[Path], ElementTree]]:
+ yield _parse
+ _etree_cache.clear()
diff --git a/tests/test_build.py b/tests/test_builders/test_build.py
index ed4bc43..3f6d12c 100644
--- a/tests/test_build.py
+++ b/tests/test_builders/test_build.py
@@ -2,13 +2,17 @@
import os
import shutil
+from contextlib import contextmanager
from unittest import mock
import pytest
from docutils import nodes
+from sphinx.cmd.build import build_main
from sphinx.errors import SphinxError
+from tests.utils import TESTS_ROOT
+
def request_session_head(url, **kwargs):
response = mock.Mock()
@@ -60,12 +64,12 @@ def test_root_doc_not_found(tmp_path, make_app):
app = make_app('dummy', srcdir=tmp_path)
with pytest.raises(SphinxError):
- app.builder.build_all() # no index.rst
+ app.build(force_all=True) # no index.rst
@pytest.mark.sphinx(buildername='text', testroot='circular')
def test_circular_toctree(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warning.getvalue()
assert (
'circular toctree references detected, ignoring: '
@@ -77,7 +81,7 @@ def test_circular_toctree(app, status, warning):
@pytest.mark.sphinx(buildername='text', testroot='numbered-circular')
def test_numbered_circular_toctree(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
warnings = warning.getvalue()
assert (
'circular toctree references detected, ignoring: '
@@ -89,7 +93,7 @@ def test_numbered_circular_toctree(app, status, warning):
@pytest.mark.sphinx(buildername='dummy', testroot='images')
def test_image_glob(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
# index.rst
doctree = app.env.get_doctree('index')
@@ -133,3 +137,29 @@ def test_image_glob(app, status, warning):
assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.svg'}
assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*'
+
+
+@contextmanager
+def force_colors():
+ forcecolor = os.environ.get('FORCE_COLOR', None)
+
+ try:
+ os.environ['FORCE_COLOR'] = '1'
+ yield
+ finally:
+ if forcecolor is None:
+ os.environ.pop('FORCE_COLOR', None)
+ else:
+ os.environ['FORCE_COLOR'] = forcecolor
+
+
+def test_log_no_ansi_colors(tmp_path):
+ with force_colors():
+ wfile = tmp_path / 'warnings.txt'
+ srcdir = TESTS_ROOT / 'roots' / 'test-nitpicky-warnings'
+ argv = list(map(str, ['-b', 'html', srcdir, tmp_path, '-n', '-w', wfile]))
+ retcode = build_main(argv)
+ assert retcode == 0
+
+ content = wfile.read_text(encoding='utf8')
+ assert '\x1b[91m' not in content
diff --git a/tests/test_build_changes.py b/tests/test_builders/test_build_changes.py
index b340c8d..b537b87 100644
--- a/tests/test_build_changes.py
+++ b/tests/test_builders/test_build_changes.py
@@ -9,7 +9,7 @@ def test_build(app):
# TODO: Use better checking of html content
htmltext = (app.outdir / 'changes.html').read_text(encoding='utf8')
- assert 'New in version 0.6: Some funny stuff.' in htmltext
+ assert 'Added in version 0.6: Some funny stuff.' in htmltext
assert 'Changed in version 0.6: Even more funny stuff.' in htmltext
assert 'Deprecated since version 0.6: Boring stuff.' in htmltext
diff --git a/tests/test_build_dirhtml.py b/tests/test_builders/test_build_dirhtml.py
index dc5ab86..dc5ab86 100644
--- a/tests/test_build_dirhtml.py
+++ b/tests/test_builders/test_build_dirhtml.py
diff --git a/tests/test_build_epub.py b/tests/test_builders/test_build_epub.py
index 7f5b815..6829f22 100644
--- a/tests/test_build_epub.py
+++ b/tests/test_builders/test_build_epub.py
@@ -22,6 +22,7 @@ def runnable(command):
class EPUBElementTree:
"""Test helper for content.opf and toc.ncx"""
+
namespaces = {
'idpf': 'http://www.idpf.org/2007/opf',
'dc': 'http://purl.org/dc/elements/1.1/',
@@ -60,7 +61,7 @@ class EPUBElementTree:
@pytest.mark.sphinx('epub', testroot='basic')
def test_build_epub(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'mimetype').read_text(encoding='utf8') == 'application/epub+zip'
assert (app.outdir / 'META-INF' / 'container.xml').exists()
@@ -277,7 +278,7 @@ def test_escaped_toc(app):
@pytest.mark.sphinx('epub', testroot='basic')
def test_epub_writing_mode(app):
# horizontal (default)
- app.builder.build_all()
+ app.build(force_all=True)
# horizontal / page-progression-direction
opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text(encoding='utf8'))
@@ -323,7 +324,7 @@ def test_epub_anchor_id(app):
@pytest.mark.sphinx('epub', testroot='html_assets')
def test_epub_assets(app):
- app.builder.build_all()
+ app.build(force_all=True)
# epub_sytlesheets (same as html_css_files)
content = (app.outdir / 'index.xhtml').read_text(encoding='utf8')
@@ -336,7 +337,7 @@ def test_epub_assets(app):
@pytest.mark.sphinx('epub', testroot='html_assets',
confoverrides={'epub_css_files': ['css/epub.css']})
def test_epub_css_files(app):
- app.builder.build_all()
+ app.build(force_all=True)
# epub_css_files
content = (app.outdir / 'index.xhtml').read_text(encoding='utf8')
@@ -361,13 +362,13 @@ def test_html_download_role(app, status, warning):
'<span class="pre">not_found.dat</span></code></p></li>' in content)
assert ('<li><p><code class="xref download docutils literal notranslate">'
'<span class="pre">Sphinx</span> <span class="pre">logo</span></code>'
- '<span class="link-target"> [http://www.sphinx-doc.org/en/master'
- '/_static/sphinxheader.png]</span></p></li>' in content)
+ '<span class="link-target"> [https://www.sphinx-doc.org/en/master'
+ '/_static/sphinx-logo.svg]</span></p></li>' in content)
@pytest.mark.sphinx('epub', testroot='toctree-duplicated')
def test_duplicated_toctree_entry(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert 'WARNING: duplicated ToC entry found: foo.xhtml' in warning.getvalue()
@@ -377,16 +378,21 @@ def test_duplicated_toctree_entry(app, status, warning):
def test_run_epubcheck(app):
app.build()
+ if not runnable(['java', '-version']):
+ pytest.skip("Unable to run Java; skipping test")
+
epubcheck = os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar')
- if runnable(['java', '-version']) and os.path.exists(epubcheck):
- try:
- subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'],
- capture_output=True, check=True)
- except CalledProcessError as exc:
- print(exc.stdout.decode('utf-8'))
- print(exc.stderr.decode('utf-8'))
- msg = f'epubcheck exited with return code {exc.returncode}'
- raise AssertionError(msg) from exc
+ if not os.path.exists(epubcheck):
+ pytest.skip("Could not find epubcheck; skipping test")
+
+ try:
+ subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'],
+ capture_output=True, check=True)
+ except CalledProcessError as exc:
+ print(exc.stdout.decode('utf-8'))
+ print(exc.stderr.decode('utf-8'))
+ msg = f'epubcheck exited with return code {exc.returncode}'
+ raise AssertionError(msg) from exc
def test_xml_name_pattern_check():
diff --git a/tests/test_build_gettext.py b/tests/test_builders/test_build_gettext.py
index 6d9154e..ddc6d30 100644
--- a/tests/test_build_gettext.py
+++ b/tests/test_builders/test_build_gettext.py
@@ -47,7 +47,7 @@ def test_Catalog_duplicated_message():
@pytest.mark.sphinx('gettext', srcdir='root-gettext')
def test_build_gettext(app):
# Generic build; should fail only when the builder is horribly broken.
- app.builder.build_all()
+ app.build(force_all=True)
# Do messages end up in the correct location?
# top-level documents end up in a message catalog
@@ -62,7 +62,7 @@ def test_build_gettext(app):
@pytest.mark.sphinx('gettext', srcdir='root-gettext')
def test_msgfmt(app):
- app.builder.build_all()
+ app.build(force_all=True)
(app.outdir / 'en' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True)
with chdir(app.outdir):
@@ -102,7 +102,7 @@ def test_msgfmt(app):
confoverrides={'gettext_compact': False})
def test_gettext_index_entries(app):
# regression test for #976
- app.builder.build(['index_entries'])
+ app.build(filenames=[app.srcdir / 'index_entries.txt'])
pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8')
msg_ids = list(filter(None, map(msgid_getter, pot.splitlines())))
@@ -131,7 +131,7 @@ def test_gettext_index_entries(app):
def test_gettext_disable_index_entries(app):
# regression test for #976
app.env._pickled_doctree_cache.clear() # clear cache
- app.builder.build(['index_entries'])
+ app.build(filenames=[app.srcdir / 'index_entries.txt'])
pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8')
msg_ids = list(filter(None, map(msgid_getter, pot.splitlines())))
@@ -147,7 +147,7 @@ def test_gettext_disable_index_entries(app):
@pytest.mark.sphinx('gettext', testroot='intl', srcdir='gettext')
def test_gettext_template(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinx.pot').is_file()
@@ -158,7 +158,7 @@ def test_gettext_template(app):
@pytest.mark.sphinx('gettext', testroot='gettext-template')
def test_gettext_template_msgid_order_in_sphinxpot(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinx.pot').is_file()
result = (app.outdir / 'sphinx.pot').read_text(encoding='utf8')
@@ -175,7 +175,7 @@ def test_gettext_template_msgid_order_in_sphinxpot(app):
'gettext', srcdir='root-gettext',
confoverrides={'gettext_compact': 'documentation'})
def test_build_single_pot(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'documentation.pot').is_file()
@@ -196,7 +196,7 @@ def test_build_single_pot(app):
confoverrides={'gettext_compact': False,
'gettext_additional_targets': ['image']})
def test_gettext_prolog_epilog_substitution(app):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'prolog_epilog_substitution.pot').is_file()
pot = (app.outdir / 'prolog_epilog_substitution.pot').read_text(encoding='utf8')
@@ -223,7 +223,7 @@ def test_gettext_prolog_epilog_substitution(app):
'gettext_additional_targets': ['image']})
def test_gettext_prolog_epilog_substitution_excluded(app):
# regression test for #9428
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'prolog_epilog_substitution_excluded.pot').is_file()
pot = (app.outdir / 'prolog_epilog_substitution_excluded.pot').read_text(encoding='utf8')
diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py
new file mode 100644
index 0000000..1fa3ba4
--- /dev/null
+++ b/tests/test_builders/test_build_html.py
@@ -0,0 +1,378 @@
+"""Test the HTML builder and check output against XPath."""
+
+import os
+import posixpath
+import re
+
+import pytest
+
+from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
+from sphinx.deprecation import RemovedInSphinx80Warning
+from sphinx.errors import ConfigError
+from sphinx.util.console import strip_colors
+from sphinx.util.inventory import InventoryFile
+
+from tests.test_builders.xpath_data import FIGURE_CAPTION
+from tests.test_builders.xpath_util import check_xpath
+
+
+def test_html4_error(make_app, tmp_path):
+ (tmp_path / 'conf.py').write_text('', encoding='utf-8')
+ with pytest.raises(
+ ConfigError,
+ match='HTML 4 is no longer supported by Sphinx',
+ ):
+ make_app(
+ buildername='html',
+ srcdir=tmp_path,
+ confoverrides={'html4_writer': True},
+ )
+
+
+@pytest.mark.parametrize(("fname", "path", "check"), [
+ ('index.html', ".//div[@class='citation']/span", r'Ref1'),
+ ('index.html', ".//div[@class='citation']/span", r'Ref_1'),
+
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
+ ('footnote.html', ".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"),
+ ('footnote.html', ".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"),
+ ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"),
+ ('footnote.html', ".//div[@class='citation']/span/a[@href='#id4']", r"bar"),
+ ('footnote.html', ".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"),
+ ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"),
+])
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_build_html_output_docutils18')
+def test_docutils_output(app, cached_etree_parse, fname, path, check):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check)
+
+
+@pytest.mark.sphinx('html', parallel=2)
+def test_html_parallel(app):
+ app.build()
+
+
+@pytest.mark.sphinx('html', testroot='build-html-translator')
+def test_html_translator(app):
+ app.build()
+ assert app.builder.docwriter.visitor.depart_with_node == 10
+
+
+@pytest.mark.parametrize("expect", [
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True),
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True),
+ (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True),
+ (".//div//span[@class='caption-number']", "No.1 ", True),
+ (".//div//span[@class='caption-number']", "No.2 ", True),
+ (".//li/p/a/span", 'Fig. 1', True),
+ (".//li/p/a/span", 'Fig. 2', True),
+ (".//li/p/a/span", 'Fig. 3', True),
+ (".//li/p/a/span", 'No.1', True),
+ (".//li/p/a/span", 'No.2', True),
+])
+@pytest.mark.sphinx('html', testroot='add_enumerable_node',
+ srcdir='test_enumerable_node')
+def test_enumerable_node(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False})
+def test_html_copy_source(app):
+ app.build(force_all=True)
+ assert not (app.outdir / '_sources' / 'index.rst.txt').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'})
+def test_html_sourcelink_suffix(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst.txt').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'})
+def test_html_sourcelink_suffix_same(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst').exists()
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''})
+def test_html_sourcelink_suffix_empty(app):
+ app.build(force_all=True)
+ assert (app.outdir / '_sources' / 'index.rst').exists()
+
+
+@pytest.mark.sphinx('html', testroot='html_entity')
+def test_html_entity(app):
+ app.build(force_all=True)
+ valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'}
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ for entity in re.findall(r'&([a-z]+);', content, re.MULTILINE):
+ assert entity not in valid_entities
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_inventory(app):
+ app.build(force_all=True)
+
+ with app.outdir.joinpath('objects.inv').open('rb') as f:
+ invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join)
+
+ assert set(invdata.keys()) == {'std:label', 'std:doc'}
+ assert set(invdata['std:label'].keys()) == {'modindex',
+ 'py-modindex',
+ 'genindex',
+ 'search'}
+ assert invdata['std:label']['modindex'] == ('Python',
+ '',
+ 'https://www.google.com/py-modindex.html',
+ 'Module Index')
+ assert invdata['std:label']['py-modindex'] == ('Python',
+ '',
+ 'https://www.google.com/py-modindex.html',
+ 'Python Module Index')
+ assert invdata['std:label']['genindex'] == ('Python',
+ '',
+ 'https://www.google.com/genindex.html',
+ 'Index')
+ assert invdata['std:label']['search'] == ('Python',
+ '',
+ 'https://www.google.com/search.html',
+ 'Search Page')
+ assert set(invdata['std:doc'].keys()) == {'index'}
+ assert invdata['std:doc']['index'] == ('Python',
+ '',
+ 'https://www.google.com/index.html',
+ 'The basic Sphinx documentation for testing')
+
+
+@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''})
+def test_html_anchor_for_figure(app):
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>'
+ '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>'
+ in content)
+
+
+@pytest.mark.sphinx('html', testroot='directives-raw')
+def test_html_raw_directive(app, status, warning):
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # standard case
+ assert 'standalone raw directive (HTML)' in result
+ assert 'standalone raw directive (LaTeX)' not in result
+
+ # with substitution
+ assert '<p>HTML: abc def ghi</p>' in result
+ assert '<p>LaTeX: abc ghi</p>' in result
+
+
+@pytest.mark.parametrize("expect", [
+ (".//link[@href='_static/persistent.css']"
+ "[@rel='stylesheet']", '', True),
+ (".//link[@href='_static/default.css']"
+ "[@rel='stylesheet']"
+ "[@title='Default']", '', True),
+ (".//link[@href='_static/alternate1.css']"
+ "[@rel='alternate stylesheet']"
+ "[@title='Alternate']", '', True),
+ (".//link[@href='_static/alternate2.css']"
+ "[@rel='alternate stylesheet']", '', True),
+ (".//link[@href='_static/more_persistent.css']"
+ "[@rel='stylesheet']", '', True),
+ (".//link[@href='_static/more_default.css']"
+ "[@rel='stylesheet']"
+ "[@title='Default']", '', True),
+ (".//link[@href='_static/more_alternate1.css']"
+ "[@rel='alternate stylesheet']"
+ "[@title='Alternate']", '', True),
+ (".//link[@href='_static/more_alternate2.css']"
+ "[@rel='alternate stylesheet']", '', True),
+])
+@pytest.mark.sphinx('html', testroot='stylesheets')
+def test_alternate_stylesheets(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
+
+
+@pytest.mark.sphinx('html', testroot='html_style')
+def test_html_style(app, status, warning):
+ app.build()
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result
+ assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />'
+ not in result)
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_sidebar(app, status, warning):
+ ctx = {}
+
+ # default for alabaster
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
+ assert '<h3>Navigation</h3>' in result
+ assert '<h3>Related Topics</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
+ 'searchbox.html', 'donate.html']
+
+ # only relations.html
+ app.config.html_sidebars = {'**': ['relations.html']}
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
+ assert '<h3>Navigation</h3>' not in result
+ assert '<h3>Related Topics</h3>' in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == ['relations.html']
+
+ # no sidebars
+ app.config.html_sidebars = {'**': []}
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<div class="sphinxsidebar" role="navigation" '
+ 'aria-label="main navigation">' not in result)
+ assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
+ assert '<h3>Navigation</h3>' not in result
+ assert '<h3>Related Topics</h3>' not in result
+ assert '<h3 id="searchlabel">Quick search</h3>' not in result
+
+ app.builder.add_sidebars('index', ctx)
+ assert ctx['sidebars'] == []
+
+
+@pytest.mark.parametrize(("fname", "expect"), [
+ ('index.html', (".//h1/em/a[@href='https://example.com/cp.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/man.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/ls.1']", '', True)),
+ ('index.html', (".//em/a[@href='https://example.com/sphinx.']", '', True)),
+])
+@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
+ 'manpages_url': 'https://example.com/{page}.{section}'})
+@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
+def test_html_manpage(app, cached_etree_parse, fname, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
+
+
+@pytest.mark.sphinx('html', testroot='toctree-glob',
+ confoverrides={'html_baseurl': 'https://example.com/'})
+def test_html_baseurl(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/index.html" />' in result
+
+ result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result
+
+
+@pytest.mark.sphinx('html', testroot='toctree-glob',
+ confoverrides={'html_baseurl': 'https://example.com/subdir',
+ 'html_file_suffix': '.htm'})
+def test_html_baseurl_and_html_file_suffix(app, status, warning):
+ app.build()
+
+ result = (app.outdir / 'index.htm').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result
+
+ result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8')
+ assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result
+
+
+@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
+def test_validate_html_extra_path(app):
+ (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
+ app.config.html_extra_path = [
+ '/path/to/not_found', # not found
+ '_static',
+ app.outdir, # outdir
+ app.outdir / '_static', # inside outdir
+ ]
+ with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
+ validate_html_extra_path(app, app.config)
+ assert app.config.html_extra_path == ['_static']
+
+
+@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
+def test_validate_html_static_path(app):
+ (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
+ app.config.html_static_path = [
+ '/path/to/not_found', # not found
+ '_static',
+ app.outdir, # outdir
+ app.outdir / '_static', # inside outdir
+ ]
+ with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
+ validate_html_static_path(app, app.config)
+ assert app.config.html_static_path == ['_static']
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_permalinks': False})
+def test_html_permalink_disable(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<h1>The basic Sphinx documentation for testing</h1>' in content
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'})
+def test_html_permalink_icon(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" '
+ 'href="#the-basic-sphinx-documentation-for-testing" '
+ 'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon')
+def test_html_signaturereturn_icon(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<span class="sig-return-icon">&#x2192;</span>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='root', srcdir=os.urandom(4).hex())
+def test_html_remove_sources_before_write_gh_issue_10786(app, warning):
+ # see: https://github.com/sphinx-doc/sphinx/issues/10786
+ target = app.srcdir / 'img.png'
+
+ def handler(app):
+ assert target.exists()
+ target.unlink()
+ return []
+
+ app.connect('html-collect-pages', handler)
+ assert target.exists()
+ app.build()
+ assert not target.exists()
+
+ ws = strip_colors(warning.getvalue()).splitlines()
+ assert len(ws) >= 1
+
+ file = os.fsdecode(target)
+ assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1]
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
new file mode 100644
index 0000000..ece6f49
--- /dev/null
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -0,0 +1,276 @@
+"""Test the HTML builder and check output against XPath."""
+
+import re
+
+import pytest
+
+from tests.test_builders.xpath_util import check_xpath
+
+
+def tail_check(check):
+ rex = re.compile(check)
+
+ def checker(nodes):
+ for node in nodes:
+ if node.tail and rex.search(node.tail):
+ return True
+ msg = f'{check!r} not found in tail of any nodes {nodes}'
+ raise AssertionError(msg)
+ return checker
+
+
+@pytest.mark.parametrize(("fname", "path", "check"), [
+ ('images.html', ".//img[@src='_images/img.png']", ''),
+ ('images.html', ".//img[@src='_images/img1.png']", ''),
+ ('images.html', ".//img[@src='_images/simg.png']", ''),
+ ('images.html', ".//img[@src='_images/svgimg.svg']", ''),
+ ('images.html', ".//a[@href='_sources/images.txt']", ''),
+
+ ('subdir/images.html', ".//img[@src='../_images/img1.png']", ''),
+ ('subdir/images.html', ".//img[@src='../_images/rimg.png']", ''),
+
+ ('subdir/includes.html', ".//a[@class='reference download internal']", ''),
+ ('subdir/includes.html', ".//img[@src='../_images/img.png']", ''),
+ ('subdir/includes.html', ".//p", 'This is an include file.'),
+ ('subdir/includes.html', ".//pre/span", 'line 1'),
+ ('subdir/includes.html', ".//pre/span", 'line 2'),
+
+ ('includes.html', ".//pre", 'Max Strauß'),
+ ('includes.html', ".//a[@class='reference download internal']", ''),
+ ('includes.html', ".//pre/span", '"quotes"'),
+ ('includes.html', ".//pre/span", "'included'"),
+ ('includes.html', ".//pre/span[@class='s2']", 'üöä'),
+ ('includes.html', ".//div[@class='inc-pyobj1 highlight-text notranslate']//pre",
+ r'^class Foo:\n pass\n\s*$'),
+ ('includes.html', ".//div[@class='inc-pyobj2 highlight-text notranslate']//pre",
+ r'^ def baz\(\):\n pass\n\s*$'),
+ ('includes.html', ".//div[@class='inc-lines highlight-text notranslate']//pre",
+ r'^class Foo:\n pass\nclass Bar:\n$'),
+ ('includes.html', ".//div[@class='inc-startend highlight-text notranslate']//pre",
+ '^foo = "Including Unicode characters: üöä"\\n$'),
+ ('includes.html', ".//div[@class='inc-preappend highlight-text notranslate']//pre",
+ r'(?m)^START CODE$'),
+ ('includes.html', ".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span",
+ r'def'),
+ ('includes.html', ".//div[@class='inc-tab3 highlight-text notranslate']//pre",
+ r'-| |-'),
+ ('includes.html', ".//div[@class='inc-tab8 highlight-python notranslate']//pre/span",
+ r'-| |-'),
+
+ ('autodoc.html', ".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
+ ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
+ ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
+ ('autodoc.html', ".//dd/p", r'Return spam\.'),
+
+ ('extapi.html', ".//strong", 'from class: Bar'),
+
+ ('markup.html', ".//title", 'set by title directive'),
+ ('markup.html', ".//p/em", 'Section author: Georg Brandl'),
+ ('markup.html', ".//p/em", 'Module author: Georg Brandl'),
+ # created by the meta directive
+ ('markup.html', ".//meta[@name='author'][@content='Me']", ''),
+ ('markup.html', ".//meta[@name='keywords'][@content='docs, sphinx']", ''),
+ # a label created by ``.. _label:``
+ ('markup.html', ".//div[@id='label']", ''),
+ # code with standard code blocks
+ ('markup.html', ".//pre", '^some code$'),
+ # an option list
+ ('markup.html', ".//span[@class='option']", '--help'),
+ # admonitions
+ ('markup.html', ".//p[@class='admonition-title']", 'My Admonition'),
+ ('markup.html', ".//div[@class='admonition note']/p", 'Note text.'),
+ ('markup.html', ".//div[@class='admonition warning']/p", 'Warning text.'),
+ # inline markup
+ ('markup.html', ".//li/p/strong", r'^command\\n$'),
+ ('markup.html', ".//li/p/strong", r'^program\\n$'),
+ ('markup.html', ".//li/p/em", r'^dfn\\n$'),
+ ('markup.html', ".//li/p/kbd", r'^kbd\\n$'),
+ ('markup.html', ".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'),
+ ('markup.html', ".//li/p/code/span[@class='pre']", '^a/$'),
+ ('markup.html', ".//li/p/code/em/span[@class='pre']", '^varpart$'),
+ ('markup.html', ".//li/p/code/em/span[@class='pre']", '^i$'),
+ ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']"
+ "[@class='pep reference external']/strong", 'PEP 8'),
+ ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']"
+ "[@class='pep reference external']/strong",
+ 'Python Enhancement Proposal #8'),
+ ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ "[@class='rfc reference external']/strong", 'RFC 1'),
+ ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ "[@class='rfc reference external']/strong", 'Request for Comments #1'),
+ ('markup.html', ".//a[@href='objects.html#envvar-HOME']"
+ "[@class='reference internal']/code/span[@class='pre']", 'HOME'),
+ ('markup.html', ".//a[@href='#with']"
+ "[@class='reference internal']/code/span[@class='pre']", '^with$'),
+ ('markup.html', ".//a[@href='#grammar-token-try_stmt']"
+ "[@class='reference internal']/code/span", '^statement$'),
+ ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
+ ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
+ ('markup.html', ".//a[@href='subdir/includes.html']"
+ "[@class='reference internal']/span", 'Including in subdir'),
+ ('markup.html', ".//a[@href='objects.html#cmdoption-python-c']"
+ "[@class='reference internal']/code/span[@class='pre']", '-c'),
+ # abbreviations
+ ('markup.html', ".//abbr[@title='abbreviation']", '^abbr$'),
+ # version stuff
+ ('markup.html', ".//div[@class='versionadded']/p/span", 'Added in version 0.6: '),
+ ('markup.html', ".//div[@class='versionadded']/p/span",
+ tail_check('First paragraph of versionadded')),
+ ('markup.html', ".//div[@class='versionchanged']/p/span",
+ tail_check('First paragraph of versionchanged')),
+ ('markup.html', ".//div[@class='versionchanged']/p",
+ 'Second paragraph of versionchanged'),
+ ('markup.html', ".//div[@class='versionremoved']/p/span", 'Removed in version 0.6: '),
+ # footnote reference
+ ('markup.html', ".//a[@class='footnote-reference brackets']", r'1'),
+ # created by reference lookup
+ ('markup.html', ".//a[@href='index.html#ref1']", ''),
+ # ``seealso`` directive
+ ('markup.html', ".//div/p[@class='admonition-title']", 'See also'),
+ # a ``hlist`` directive
+ ('markup.html', ".//table[@class='hlist']/tr/td/ul/li/p", '^This$'),
+ # a ``centered`` directive
+ ('markup.html', ".//p[@class='centered']/strong", 'LICENSE'),
+ # a glossary
+ ('markup.html', ".//dl/dt[@id='term-boson']", 'boson'),
+ ('markup.html', ".//dl/dt[@id='term-boson']/a", '¶'),
+ # a production list
+ ('markup.html', ".//pre/strong", 'try_stmt'),
+ ('markup.html', ".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
+ # tests for ``only`` directive
+ ('markup.html', ".//p", 'A global substitution!'),
+ ('markup.html', ".//p", 'In HTML.'),
+ ('markup.html', ".//p", 'In both.'),
+ ('markup.html', ".//p", 'Always present'),
+ # tests for ``any`` role
+ ('markup.html', ".//a[@href='#with']/span", 'headings'),
+ ('markup.html', ".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
+ # tests for numeric labels
+ ('markup.html', ".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
+ # tests for smartypants
+ ('markup.html', ".//li/p", 'Smart “quotes” in English ‘text’.'),
+ ('markup.html', ".//li/p", 'Smart — long and – short dashes.'),
+ ('markup.html', ".//li/p", 'Ellipsis…'),
+ ('markup.html', ".//li/p/code/span[@class='pre']", 'foo--"bar"...'),
+ ('markup.html', ".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'),
+ ('markup.html', ".//p", 'Il dit : « C’est “super” ! »'),
+
+ ('objects.html', ".//dt[@id='mod.Cls.meth1']", ''),
+ ('objects.html', ".//dt[@id='errmod.Error']", ''),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'),
+ ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'),
+ ('objects.html', ".//a[@href='#mod.Cls'][@class='reference internal']", ''),
+ ('objects.html', ".//dl[@class='std userdesc']", ''),
+ ('objects.html', ".//dt[@id='userdesc-myobj']", ''),
+ ('objects.html', ".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
+ # docfields
+ ('objects.html', ".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#Time']", 'Time'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
+ # C references
+ ('objects.html', ".//span[@class='pre']", 'CFunction()'),
+ ('objects.html', ".//a[@href='#c.Sphinx_DoSomething']", ''),
+ ('objects.html', ".//a[@href='#c.SphinxStruct.member']", ''),
+ ('objects.html', ".//a[@href='#c.SPHINX_USE_PYTHON']", ''),
+ ('objects.html', ".//a[@href='#c.SphinxType']", ''),
+ ('objects.html', ".//a[@href='#c.sphinx_global']", ''),
+ # test global TOC created by toctree()
+ ('objects.html', ".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']",
+ 'Testing object descriptions'),
+ ('objects.html', ".//li[@class='toctree-l1']/a[@href='markup.html']",
+ 'Testing various markup'),
+ # test unknown field names
+ ('objects.html', ".//dt[@class='field-odd']", 'Field_name'),
+ ('objects.html', ".//dt[@class='field-even']", 'Field_name all lower'),
+ ('objects.html', ".//dt[@class='field-odd']", 'FIELD_NAME'),
+ ('objects.html', ".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
+ ('objects.html', ".//dt[@class='field-odd']", 'Field_Name'),
+ ('objects.html', ".//dt[@class='field-even']", 'Field_Name All Word Caps'),
+ # ('objects.html', ".//dt[@class='field-odd']", 'Field_name'), (duplicate)
+ ('objects.html', ".//dt[@class='field-even']", 'Field_name First word cap'),
+ ('objects.html', ".//dt[@class='field-odd']", 'FIELd_name'),
+ ('objects.html', ".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
+ # custom sidebar
+ ('objects.html', ".//h4", 'Custom sidebar'),
+ # docfields
+ ('objects.html', ".//dd[@class='field-odd']/p/strong", '^moo$'),
+ ('objects.html', ".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
+ ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
+ # others
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
+ 'perl'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
+ '\\+p'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span",
+ '--ObjC\\+\\+'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
+ '--plugin.option'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
+ "/code/span",
+ 'create-auth-token'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
+ 'arg'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
+ '-j'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
+ 'hg'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
+ 'commit'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ 'git'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ 'commit'),
+ ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
+ '-p'),
+
+ ('index.html', ".//meta[@name='hc'][@content='hcval']", ''),
+ ('index.html', ".//meta[@name='hc_co'][@content='hcval_co']", ''),
+ ('index.html', ".//li[@class='toctree-l1']/a", 'Testing various markup'),
+ ('index.html', ".//li[@class='toctree-l2']/a", 'Inline markup'),
+ ('index.html', ".//title", 'Sphinx <Tests>'),
+ ('index.html', ".//div[@class='footer']", 'copyright text credits'),
+ ('index.html', ".//a[@href='https://python.org/']"
+ "[@class='reference external']", ''),
+ ('index.html', ".//li/p/a[@href='genindex.html']/span", 'Index'),
+ ('index.html', ".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
+ # custom sidebar only for contents
+ ('index.html', ".//h4", 'Contents sidebar'),
+ # custom JavaScript
+ ('index.html', ".//script[@src='file://moo.js']", ''),
+ # URL in contents
+ ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/']",
+ 'https://sphinx-doc.org/'),
+ ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/latest/']",
+ 'Latest reference'),
+ # Indirect hyperlink targets across files
+ ('index.html', ".//a[@href='markup.html#some-label'][@class='reference internal']/span",
+ '^indirect hyperref$'),
+
+ ('bom.html', ".//title", " File with UTF-8 BOM"),
+
+ ('extensions.html', ".//a[@href='https://python.org/dev/']", "https://python.org/dev/"),
+ ('extensions.html', ".//a[@href='https://bugs.python.org/issue1000']", "issue 1000"),
+ ('extensions.html', ".//a[@href='https://bugs.python.org/issue1042']", "explicit caption"),
+
+ # index entries
+ ('genindex.html', ".//a/strong", "Main"),
+ ('genindex.html', ".//a/strong", "[1]"),
+ ('genindex.html', ".//a/strong", "Other"),
+ ('genindex.html', ".//a", "entry"),
+ ('genindex.html', ".//li/a", "double"),
+
+ ('otherext.html', ".//h1", "Generated section"),
+ ('otherext.html', ".//a[@href='_sources/otherext.foo.txt']", ''),
+
+ ('search.html', ".//meta[@name='robots'][@content='noindex']", ''),
+])
+@pytest.mark.sphinx('html', tags=['testtag'],
+ confoverrides={'html_context.hckey_co': 'hcval_co'})
+@pytest.mark.test_params(shared_result='test_build_html_output')
+def test_html5_output(app, cached_etree_parse, fname, path, check):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check)
diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py
new file mode 100644
index 0000000..fc7a987
--- /dev/null
+++ b/tests/test_builders/test_build_html_assets.py
@@ -0,0 +1,152 @@
+"""Test the HTML builder and check output against XPath."""
+
+import re
+from pathlib import Path
+
+import pytest
+
+import sphinx.builders.html
+from sphinx.builders.html._assets import _file_checksum
+from sphinx.errors import ThemeError
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_html_assets(app):
+ app.build(force_all=True)
+
+ # exclude_path and its family
+ assert not (app.outdir / 'static' / 'index.html').exists()
+ assert not (app.outdir / 'extra' / 'index.html').exists()
+
+ # html_static_path
+ assert not (app.outdir / '_static' / '.htaccess').exists()
+ assert not (app.outdir / '_static' / '.htpasswd').exists()
+ assert (app.outdir / '_static' / 'API.html').exists()
+ assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4'
+ assert (app.outdir / '_static' / 'css' / 'style.css').exists()
+ assert (app.outdir / '_static' / 'js' / 'custom.js').exists()
+ assert (app.outdir / '_static' / 'rimg.png').exists()
+ assert not (app.outdir / '_static' / '_build' / 'index.html').exists()
+ assert (app.outdir / '_static' / 'background.png').exists()
+ assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists()
+ assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists()
+
+ # html_extra_path
+ assert (app.outdir / '.htaccess').exists()
+ assert not (app.outdir / '.htpasswd').exists()
+ assert (app.outdir / 'API.html_t').exists()
+ assert (app.outdir / 'css/style.css').exists()
+ assert (app.outdir / 'rimg.png').exists()
+ assert not (app.outdir / '_build' / 'index.html').exists()
+ assert (app.outdir / 'background.png').exists()
+ assert (app.outdir / 'subdir' / '.htaccess').exists()
+ assert not (app.outdir / 'subdir' / '.htpasswd').exists()
+
+ # html_css_files
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content
+ assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
+ 'href="https://example.com/custom.css" />' in content)
+
+ # html_js_files
+ assert '<script src="_static/js/custom.js"></script>' in content
+ assert ('<script async="async" src="https://example.com/script.js">'
+ '</script>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_assets_order(app, monkeypatch):
+ monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
+
+ app.add_css_file('normal.css')
+ app.add_css_file('early.css', priority=100)
+ app.add_css_file('late.css', priority=750)
+ app.add_css_file('lazy.css', priority=900)
+ app.add_js_file('normal.js')
+ app.add_js_file('early.js', priority=100)
+ app.add_js_file('late.js', priority=750)
+ app.add_js_file('lazy.js', priority=900)
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # css_files
+ expected = [
+ '_static/early.css',
+ '_static/pygments.css',
+ '_static/alabaster.css',
+ 'https://example.com/custom.css',
+ '_static/normal.css',
+ '_static/late.css',
+ '_static/css/style.css',
+ '_static/lazy.css',
+ ]
+ pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected)
+ assert re.search(pattern, content, re.DOTALL), content
+
+ # js_files
+ expected = [
+ '_static/early.js',
+ '_static/doctools.js',
+ '_static/sphinx_highlight.js',
+ 'https://example.com/script.js',
+ '_static/normal.js',
+ '_static/late.js',
+ '_static/js/custom.js',
+ '_static/lazy.js',
+ ]
+ pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected)
+ assert re.search(pattern, content, re.DOTALL), content
+
+
+@pytest.mark.sphinx('html', testroot='html_file_checksum')
+def test_file_checksum(app):
+ app.add_css_file('stylesheet-a.css')
+ app.add_css_file('stylesheet-b.css')
+ app.add_css_file('https://example.com/custom.css')
+ app.add_js_file('script.js')
+ app.add_js_file('empty.js')
+ app.add_js_file('https://example.com/script.js')
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # checksum for local files
+ assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content
+ assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content
+ assert '<script src="_static/script.js?v=48278d48"></script>' in content
+
+ # empty files have no checksum
+ assert '<script src="_static/empty.js"></script>' in content
+
+ # no checksum for hyperlinks
+ assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content
+ assert '<script src="https://example.com/script.js"></script>' in content
+
+
+def test_file_checksum_query_string():
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path(), 'with_query_string.css?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path(), 'with_query_string.js?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1')
+
+ with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
+ _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1')
+
+
+@pytest.mark.sphinx('html', testroot='html_assets')
+def test_javscript_loading_method(app):
+ app.add_js_file('normal.js')
+ app.add_js_file('early.js', loading_method='async')
+ app.add_js_file('late.js', loading_method='defer')
+
+ app.build(force_all=True)
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<script src="_static/normal.js"></script>' in content
+ assert '<script async="async" src="_static/early.js"></script>' in content
+ assert '<script defer="defer" src="_static/late.js"></script>' in content
diff --git a/tests/test_builders/test_build_html_code.py b/tests/test_builders/test_build_html_code.py
new file mode 100644
index 0000000..f07eb97
--- /dev/null
+++ b/tests/test_builders/test_build_html_code.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-block',
+ confoverrides={'html_codeblock_linenos_style': 'table'})
+def test_html_codeblock_linenos_style_table(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n'
+ '<span class="normal">2</span>\n'
+ '<span class="normal">3</span>\n'
+ '<span class="normal">4</span></pre></div>') in content
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-block',
+ confoverrides={'html_codeblock_linenos_style': 'inline'})
+def test_html_codeblock_linenos_style_inline(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ assert '<span class="linenos">1</span>' in content
+
+
+@pytest.mark.sphinx('html', testroot='reST-code-role')
+def test_html_code_role(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ common_content = (
+ '<span class="k">def</span> <span class="nf">foo</span>'
+ '<span class="p">(</span>'
+ '<span class="mi">1</span> '
+ '<span class="o">+</span> '
+ '<span class="mi">2</span> '
+ '<span class="o">+</span> '
+ '<span class="kc">None</span> '
+ '<span class="o">+</span> '
+ '<span class="s2">&quot;abc&quot;</span>'
+ '<span class="p">):</span> '
+ '<span class="k">pass</span>')
+ assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' +
+ common_content + '</code> code block</p>') in content
+ assert ('<div class="highlight-python notranslate">' +
+ '<div class="highlight"><pre><span></span>' +
+ common_content) in content
diff --git a/tests/test_builders/test_build_html_download.py b/tests/test_builders/test_build_html_download.py
new file mode 100644
index 0000000..1201c66
--- /dev/null
+++ b/tests/test_builders/test_build_html_download.py
@@ -0,0 +1,62 @@
+import hashlib
+import re
+
+import pytest
+
+
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_build_html_output')
+def test_html_download(app):
+ app.build()
+
+ # subdir/includes.html
+ result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8')
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="../(_downloads/.*/img.png)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1)).exists()
+ filename = matched.group(1)
+
+ # includes.html
+ result = (app.outdir / 'includes.html').read_text(encoding='utf8')
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="(_downloads/.*/img.png)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1)).exists()
+ assert matched.group(1) == filename
+
+ pattern = ('<a class="reference download internal" download="" '
+ 'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">')
+ matched = re.search(pattern, result)
+ assert matched
+ assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists()
+
+
+@pytest.mark.sphinx('html', testroot='roles-download')
+def test_html_download_role(app, status, warning):
+ app.build()
+ digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest()
+ assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists()
+ digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest()
+ assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists()
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert (('<li><p><a class="reference download internal" download="" '
+ 'href="_downloads/%s/dummy.dat">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">dummy.dat</span></code></a></p></li>' % digest)
+ in content)
+ assert (('<li><p><a class="reference download internal" download="" '
+ 'href="_downloads/%s/dummy.dat">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">another/dummy.dat</span></code></a></p></li>' %
+ digest_another) in content)
+ assert ('<li><p><code class="xref download docutils literal notranslate">'
+ '<span class="pre">not_found.dat</span></code></p></li>' in content)
+ assert ('<li><p><a class="reference download external" download="" '
+ 'href="https://www.sphinx-doc.org/en/master/_static/sphinx-logo.svg">'
+ '<code class="xref download docutils literal notranslate">'
+ '<span class="pre">Sphinx</span> <span class="pre">logo</span>'
+ '</code></a></p></li>' in content)
diff --git a/tests/test_builders/test_build_html_highlight.py b/tests/test_builders/test_build_html_highlight.py
new file mode 100644
index 0000000..aee1ece
--- /dev/null
+++ b/tests/test_builders/test_build_html_highlight.py
@@ -0,0 +1,61 @@
+from unittest.mock import ANY, call, patch
+
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_pygments_style_default(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'Alabaster'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'pygments_style': 'sphinx'})
+def test_html_pygments_style_manually(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'SphinxStyle'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'html_theme': 'classic'})
+def test_html_pygments_for_classic_theme(app):
+ style = app.builder.highlighter.formatter_args.get('style')
+ assert style.__name__ == 'SphinxStyle'
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_html_dark_pygments_style_default(app):
+ assert app.builder.dark_highlighter is None
+
+
+@pytest.mark.sphinx('html', testroot='highlight_options')
+def test_highlight_options(app):
+ subject = app.builder.highlighter
+ with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
+ app.build()
+
+ call_args = highlight.call_args_list
+ assert len(call_args) == 3
+ assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
+ location=ANY, opts={'default_option': True})
+ assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
+ location=ANY, opts={'python_option': True})
+ assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
+ location=ANY, opts={})
+
+
+@pytest.mark.sphinx('html', testroot='highlight_options',
+ confoverrides={'highlight_options': {'default_option': True}})
+def test_highlight_options_old(app):
+ subject = app.builder.highlighter
+ with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
+ app.build()
+
+ call_args = highlight.call_args_list
+ assert len(call_args) == 3
+ assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
+ location=ANY, opts={'default_option': True})
+ assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
+ location=ANY, opts={})
+ assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
+ location=ANY, opts={})
diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py
new file mode 100644
index 0000000..08ed618
--- /dev/null
+++ b/tests/test_builders/test_build_html_image.py
@@ -0,0 +1,80 @@
+import re
+from pathlib import Path
+
+import docutils
+import pytest
+
+
+@pytest.mark.sphinx('html', testroot='images')
+def test_html_remote_images(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img alt="http://localhost:7777/sphinx.png" '
+ 'src="http://localhost:7777/sphinx.png" />' in result)
+ assert not (app.outdir / 'sphinx.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='image-escape')
+def test_html_encoded_image(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result)
+ assert (app.outdir / '_images/img_#1.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='remote-logo')
+def test_html_remote_logo(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result)
+ assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result)
+ assert not (app.outdir / 'python-logo.png').exists()
+
+
+@pytest.mark.sphinx('html', testroot='local-logo')
+def test_html_local_logo(app, status, warning):
+ app.build(force_all=True)
+
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result)
+ assert (app.outdir / '_static/img.png').exists()
+
+
+@pytest.mark.sphinx(testroot='html_scaled_image_link')
+def test_html_scaled_image_link(app):
+ app.build()
+ context = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ # no scaled parameters
+ assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)
+
+ # scaled_image_link
+ # Docutils 0.21 adds a newline before the closing </a> tag
+ closing_space = "\n" if docutils.__version_info__[:2] >= (0, 21) else ""
+ assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
+ '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" />'
+ f'{closing_space}</a>',
+ context)
+
+ # no-scaled-link class disables the feature
+ assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
+ ' src="_images/img.png" style="[^"]+" />',
+ context)
+
+
+@pytest.mark.sphinx('html', testroot='images')
+def test_copy_images(app, status, warning):
+ app.build()
+
+ images_dir = Path(app.outdir) / '_images'
+ images = {image.name for image in images_dir.rglob('*')}
+ assert images == {
+ 'img.png',
+ 'rimg.png',
+ 'rimg1.png',
+ 'svgimg.svg',
+ 'testimäge.png',
+ }
diff --git a/tests/test_builders/test_build_html_maths.py b/tests/test_builders/test_build_html_maths.py
new file mode 100644
index 0000000..900846b
--- /dev/null
+++ b/tests/test_builders/test_build_html_maths.py
@@ -0,0 +1,58 @@
+import pytest
+
+from sphinx.errors import ConfigError
+
+
+@pytest.mark.sphinx('html', testroot='basic')
+def test_default_html_math_renderer(app, status, warning):
+ assert app.builder.math_renderer_name == 'mathjax'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.mathjax']})
+def test_html_math_renderer_is_mathjax(app, status, warning):
+ assert app.builder.math_renderer_name == 'mathjax'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.imgmath']})
+def test_html_math_renderer_is_imgmath(app, status, warning):
+ assert app.builder.math_renderer_name == 'imgmath'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.imgmath']})
+def test_html_math_renderer_is_duplicated(make_app, app_params):
+ args, kwargs = app_params
+ with pytest.raises(
+ ConfigError,
+ match='Many math_renderers are registered. But no math_renderer is selected.',
+ ):
+ make_app(*args, **kwargs)
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinx.ext.imgmath',
+ 'sphinx.ext.mathjax']})
+def test_html_math_renderer_is_duplicated2(app, status, warning):
+ # case of both mathjax and another math_renderer is loaded
+ assert app.builder.math_renderer_name == 'imgmath' # The another one is chosen
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.imgmath'],
+ 'html_math_renderer': 'imgmath'})
+def test_html_math_renderer_is_chosen(app, status, warning):
+ assert app.builder.math_renderer_name == 'imgmath'
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ confoverrides={'extensions': ['sphinxcontrib.jsmath',
+ 'sphinx.ext.mathjax'],
+ 'html_math_renderer': 'imgmath'})
+def test_html_math_renderer_is_mismatched(make_app, app_params):
+ args, kwargs = app_params
+ with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."):
+ make_app(*args, **kwargs)
diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py
new file mode 100644
index 0000000..62e68cb
--- /dev/null
+++ b/tests/test_builders/test_build_html_numfig.py
@@ -0,0 +1,487 @@
+"""Test the HTML builder and check output against XPath."""
+
+import os
+import re
+
+import pytest
+
+from tests.test_builders.xpath_data import FIGURE_CAPTION
+from tests.test_builders.xpath_util import check_xpath
+
+
+@pytest.mark.sphinx('html', testroot='numfig')
+@pytest.mark.test_params(shared_result='test_build_html_numfig')
+def test_numfig_disabled_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('index.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+ ('index.html', ".//li/p/code/span", '^fig1$', True),
+ ('index.html', ".//li/p/code/span", '^Figure%s$', True),
+ ('index.html', ".//li/p/code/span", '^table-1$', True),
+ ('index.html', ".//li/p/code/span", '^Table:%s$', True),
+ ('index.html', ".//li/p/code/span", '^CODE_1$', True),
+ ('index.html', ".//li/p/code/span", '^Code-%s$', True),
+ ('index.html', ".//li/p/a/span", '^Section 1$', True),
+ ('index.html', ".//li/p/a/span", '^Section 2.1$', True),
+ ('index.html', ".//li/p/code/span", '^Fig.{number}$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']", None, True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", None, True),
+])
+@pytest.mark.sphinx('html', testroot='numfig')
+@pytest.mark.test_params(shared_result='test_build_html_numfig')
+def test_numfig_disabled(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx(
+ 'html', testroot='numfig',
+ srcdir='test_numfig_without_numbered_toctree_warn',
+ confoverrides={'numfig': True})
+def test_numfig_without_numbered_toctree_warn(app, warning):
+ app.build()
+ # remove :numbered: option
+ index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
+ index = re.sub(':numbered:.*', '', index)
+ (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
+ app.build()
+
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 9 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 10 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 9 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 10 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 9$', True),
+ ('index.html', ".//li/p/a/span", '^Figure6$', True),
+ ('index.html', ".//li/p/a/span", '^Table 9$', True),
+ ('index.html', ".//li/p/a/span", '^Table:6$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 9$', True),
+ ('index.html', ".//li/p/a/span", '^Code-6$', True),
+ ('index.html', ".//li/p/code/span", '^foo$', True),
+ ('index.html', ".//li/p/code/span", '^bar_a$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.9 should be Fig.1$', True),
+ ('index.html', ".//li/p/code/span", '^Sect.{number}$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 5 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 7 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 8 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 5 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 7 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 8 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 6 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 6 $', True),
+])
+@pytest.mark.sphinx(
+ 'html', testroot='numfig',
+ srcdir='test_numfig_without_numbered_toctree',
+ confoverrides={'numfig': True})
+def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found):
+ # remove :numbered: option
+ index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
+ index = re.sub(':numbered:.*', '', index)
+ (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
+
+ if not os.listdir(app.outdir):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_numbered_toctree_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Table 1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Section.1$', True),
+ ('index.html', ".//li/p/a/span", '^Section.2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.4 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.4 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig', confoverrides={
+ 'numfig': True,
+ 'numfig_format': {'figure': 'Figure:%s',
+ 'table': 'Tab_%s',
+ 'code-block': 'Code-%s',
+ 'section': 'SECTION-%s'}})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
+def test_numfig_with_prefix_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2 $', True),
+ ('index.html', ".//li/p/a/span", '^Figure:1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Tab_1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.2$', True),
+ ('index.html', ".//li/p/a/span", '^Code-1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.2$', True),
+ ('index.html', ".//li/p/a/span", '^SECTION-1$', True),
+ ('index.html', ".//li/p/a/span", '^SECTION-2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.3 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_1.4 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.3 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-1.4 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.4 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.4 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Tab_2.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Code-2.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True,
+ 'numfig_format': {'figure': 'Figure:%s',
+ 'table': 'Tab_%s',
+ 'code-block': 'Code-%s',
+ 'section': 'SECTION-%s'}})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
+def test_numfig_with_prefix(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
+def test_numfig_with_secnum_depth_warn(app, warning):
+ app.build()
+ warnings = warning.getvalue()
+ assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
+ assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
+ assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
+ assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ ('index.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ ('index.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ ('index.html', ".//li/p/a/span", '^Fig. 1$', True),
+ ('index.html', ".//li/p/a/span", '^Figure2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Table 1$', True),
+ ('index.html', ".//li/p/a/span", '^Table:2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Listing 1$', True),
+ ('index.html', ".//li/p/a/span", '^Code-2.1.2$', True),
+ ('index.html', ".//li/p/a/span", '^Section.1$', True),
+ ('index.html', ".//li/p/a/span", '^Section.2.1$', True),
+ ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True),
+
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True),
+ ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1.1 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.1.2 $', True),
+ ('foo.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 1.2.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1.1 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1.2 $', True),
+ ('foo.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2.1 $', True),
+
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True),
+ ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.1 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.3 $', True),
+ ('bar.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.2.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.1 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.3 $', True),
+ ('bar.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2.1 $', True),
+
+ ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True),
+ ('baz.html', ".//table/caption/span[@class='caption-number']",
+ '^Table 2.1.2 $', True),
+ ('baz.html', ".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1.2 $', True),
+])
+@pytest.mark.sphinx('html', testroot='numfig',
+ confoverrides={'numfig': True,
+ 'numfig_secnum_depth': 2})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
+def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.parametrize("expect", [
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2 $', True),
+ (".//li/p/a/span", '^Fig. 1$', True),
+ (".//li/p/a/span", '^Figure2.2$', True),
+ (".//li/p/a/span", '^Table 1$', True),
+ (".//li/p/a/span", '^Table:2.2$', True),
+ (".//li/p/a/span", '^Listing 1$', True),
+ (".//li/p/a/span", '^Code-2.2$', True),
+ (".//li/p/a/span", '^Section.1$', True),
+ (".//li/p/a/span", '^Section.2.1$', True),
+ (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
+ (".//li/p/a/span", '^Sect.1 Foo$', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.3 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 1.4 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.3 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 1.4 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.1 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.3 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.4 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.1 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.3 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.4 $', True),
+ (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
+ (".//table/caption/span[@class='caption-number']",
+ '^Table 2.2 $', True),
+ (".//div[@class='code-block-caption']/"
+ "span[@class='caption-number']", '^Listing 2.2 $', True),
+])
+@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True})
+@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
+def test_numfig_with_singlehtml(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py
new file mode 100644
index 0000000..4d99995
--- /dev/null
+++ b/tests/test_builders/test_build_html_tocdepth.py
@@ -0,0 +1,94 @@
+"""Test the HTML builder and check output against XPath."""
+
+import pytest
+
+from tests.test_builders.xpath_util import check_xpath
+
+
+@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [
+ ('index.html', ".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
+ ('index.html', ".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
+ ('index.html', ".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
+ ('index.html', ".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
+
+ ('foo.html', ".//h1", 'Foo', True),
+ ('foo.html', ".//h2", 'Foo A', True),
+ ('foo.html', ".//h3", 'Foo A1', True),
+ ('foo.html', ".//h2", 'Foo B', True),
+ ('foo.html', ".//h3", 'Foo B1', True),
+
+ ('foo.html', ".//h1//span[@class='section-number']", '1. ', True),
+ ('foo.html', ".//h2//span[@class='section-number']", '1.1. ', True),
+ ('foo.html', ".//h3//span[@class='section-number']", '1.1.1. ', True),
+ ('foo.html', ".//h2//span[@class='section-number']", '1.2. ', True),
+ ('foo.html', ".//h3//span[@class='section-number']", '1.2.1. ', True),
+
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True),
+ ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True),
+
+ ('bar.html', ".//h1", 'Bar', True),
+ ('bar.html', ".//h2", 'Bar A', True),
+ ('bar.html', ".//h2", 'Bar B', True),
+ ('bar.html', ".//h3", 'Bar B1', True),
+ ('bar.html', ".//h1//span[@class='section-number']", '2. ', True),
+ ('bar.html', ".//h2//span[@class='section-number']", '2.1. ', True),
+ ('bar.html', ".//h2//span[@class='section-number']", '2.2. ', True),
+ ('bar.html', ".//h3//span[@class='section-number']", '2.2.1. ', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True),
+ ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False),
+
+ ('baz.html', ".//h1", 'Baz A', True),
+ ('baz.html', ".//h1//span[@class='section-number']", '2.1.1. ', True),
+])
+@pytest.mark.sphinx('html', testroot='tocdepth')
+@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
+def test_tocdepth(app, cached_etree_parse, fname, path, check, be_found):
+ app.build()
+ # issue #1251
+ check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
+
+
+@pytest.mark.parametrize("expect", [
+ (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
+ (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
+ (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
+ (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
+
+ # index.rst
+ (".//h1", 'test-tocdepth', True),
+
+ # foo.rst
+ (".//h2", 'Foo', True),
+ (".//h3", 'Foo A', True),
+ (".//h4", 'Foo A1', True),
+ (".//h3", 'Foo B', True),
+ (".//h4", 'Foo B1', True),
+ (".//h2//span[@class='section-number']", '1. ', True),
+ (".//h3//span[@class='section-number']", '1.1. ', True),
+ (".//h4//span[@class='section-number']", '1.1.1. ', True),
+ (".//h3//span[@class='section-number']", '1.2. ', True),
+ (".//h4//span[@class='section-number']", '1.2.1. ', True),
+
+ # bar.rst
+ (".//h2", 'Bar', True),
+ (".//h3", 'Bar A', True),
+ (".//h3", 'Bar B', True),
+ (".//h4", 'Bar B1', True),
+ (".//h2//span[@class='section-number']", '2. ', True),
+ (".//h3//span[@class='section-number']", '2.1. ', True),
+ (".//h3//span[@class='section-number']", '2.2. ', True),
+ (".//h4//span[@class='section-number']", '2.2.1. ', True),
+
+ # baz.rst
+ (".//h4", 'Baz A', True),
+ (".//h4//span[@class='section-number']", '2.1.1. ', True),
+])
+@pytest.mark.sphinx('singlehtml', testroot='tocdepth')
+@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
+def test_tocdepth_singlehtml(app, cached_etree_parse, expect):
+ app.build()
+ check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect)
diff --git a/tests/test_build_latex.py b/tests/test_builders/test_build_latex.py
index e37a97e..0776c74 100644
--- a/tests/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -1,9 +1,9 @@
"""Test the build process with LaTeX builder with the test root."""
+import http.server
import os
import re
import subprocess
-from itertools import chain, product
from pathlib import Path
from shutil import copyfile
from subprocess import CalledProcessError
@@ -15,36 +15,26 @@ from sphinx.config import Config
from sphinx.errors import SphinxError
from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping
from sphinx.ext.intersphinx import setup as intersphinx_setup
-from sphinx.testing.util import strip_escseq
from sphinx.util.osutil import ensuredir
from sphinx.writers.latex import LaTeXTranslator
-from .test_build_html import ENV_WARNINGS
+from tests.utils import http_server
try:
from contextlib import chdir
except ImportError:
from sphinx.util.osutil import _chdir as chdir
-LATEX_ENGINES = ['pdflatex', 'lualatex', 'xelatex']
-DOCCLASSES = ['manual', 'howto']
STYLEFILES = ['article.cls', 'fancyhdr.sty', 'titlesec.sty', 'amsmath.sty',
'framed.sty', 'color.sty', 'fancyvrb.sty',
'fncychap.sty', 'geometry.sty', 'kvoptions.sty', 'hyperref.sty',
'booktabs.sty']
-LATEX_WARNINGS = ENV_WARNINGS + """\
-%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
-%(root)s/index.rst:\\d+: WARNING: citation not found: missing
-%(root)s/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\*
-%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
-"""
-
# only run latex if all needed packages are there
def kpsetest(*filenames):
try:
- subprocess.run(['kpsewhich'] + list(filenames), capture_output=True, check=True)
+ subprocess.run(['kpsewhich', *list(filenames)], capture_output=True, check=True)
return True
except (OSError, CalledProcessError):
return False # command not found or exit with non-zero
@@ -64,7 +54,7 @@ def compile_latex_document(app, filename='python.tex', docclass='manual'):
args = [app.config.latex_engine,
'--halt-on-error',
'--interaction=nonstopmode',
- '-output-directory=%s' % latex_outputdir,
+ f'-output-directory={latex_outputdir}',
filename]
subprocess.run(args, capture_output=True, check=True)
except OSError as exc: # most likely the latex executable was not found
@@ -92,6 +82,28 @@ def skip_if_stylefiles_notfound(testfunc):
return testfunc
+class RemoteImageHandler(http.server.BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ content, content_type = None, None
+ if self.path == "/sphinx.png":
+ with open("tests/roots/test-local-logo/images/img.png", "rb") as f:
+ content = f.read()
+ content_type = "image/png"
+
+ if content:
+ self.send_response(200, "OK")
+ self.send_header("Content-Length", str(len(content)))
+ self.send_header("Content-Type", content_type)
+ self.end_headers()
+ self.wfile.write(content)
+ else:
+ self.send_response(404, "Not Found")
+ self.send_header("Content-Length", "0")
+ self.end_headers()
+
+
@skip_if_requested
@skip_if_stylefiles_notfound
@pytest.mark.parametrize(
@@ -99,13 +111,17 @@ def skip_if_stylefiles_notfound(testfunc):
# Only running test with `python_maximum_signature_line_length` not None with last
# LaTeX engine to reduce testing time, as if this configuration does not fail with
# one engine, it's almost impossible it would fail with another.
- chain(
- product(LATEX_ENGINES[:-1], DOCCLASSES, [None]),
- product([LATEX_ENGINES[-1]], DOCCLASSES, [1]),
- ),
+ [
+ ('pdflatex', 'manual', None),
+ ('pdflatex', 'howto', None),
+ ('lualatex', 'manual', None),
+ ('lualatex', 'howto', None),
+ ('xelatex', 'manual', 1),
+ ('xelatex', 'howto', 1),
+ ],
)
@pytest.mark.sphinx('latex', freshenv=True)
-def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_signature_line_length):
+def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length):
app.config.python_maximum_signature_line_length = python_maximum_signature_line_length
app.config.intersphinx_mapping = {
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
@@ -121,7 +137,8 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_
load_mappings(app)
app.builder.init()
LaTeXTranslator.ignore_missing_images = True
- app.builder.build_all()
+ with http_server(RemoteImageHandler):
+ app.build(force_all=True)
# file from latex_additional_files
assert (app.outdir / 'svgimg.svg').is_file()
@@ -131,7 +148,7 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_
@pytest.mark.sphinx('latex')
def test_writer(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8')
assert ('\\begin{sphinxfigure-in-table}\n\\centering\n\\capstart\n'
@@ -165,7 +182,7 @@ def test_writer(app, status, warning):
'\\sphinxAtStartPar\n'
'something, something else, something more\n'
'\\begin{description}\n'
- '\\sphinxlineitem{\\sphinxhref{http://www.google.com}{Google}}\n'
+ '\\sphinxlineitem{\\sphinxhref{https://www.google.com}{Google}}\n'
'\\sphinxAtStartPar\n'
'For everything.\n'
'\n'
@@ -173,22 +190,9 @@ def test_writer(app, status, warning):
'\n\n\\end{sphinxseealso}\n\n' in result)
-@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True)
-def test_latex_warnings(app, status, warning):
- app.builder.build_all()
-
- warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
- warnings_exp = LATEX_WARNINGS % {
- 'root': re.escape(app.srcdir.as_posix())}
- assert re.match(warnings_exp + '$', warnings), \
- "Warnings don't match:\n" + \
- '--- Expected (regex):\n' + warnings_exp + \
- '--- Got:\n' + warnings
-
-
@pytest.mark.sphinx('latex', testroot='basic')
def test_latex_basic(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -203,7 +207,7 @@ def test_latex_basic(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')],
})
def test_latex_basic_manual(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{report}' in result
@@ -215,7 +219,7 @@ def test_latex_basic_manual(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')],
})
def test_latex_basic_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{article}' in result
@@ -228,7 +232,7 @@ def test_latex_basic_howto(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')],
})
def test_latex_basic_manual_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{ujbook}' in result
@@ -241,7 +245,7 @@ def test_latex_basic_manual_ja(app, status, warning):
'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')],
})
def test_latex_basic_howto_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{ujreport}' in result
@@ -250,7 +254,7 @@ def test_latex_basic_howto_ja(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-theme')
def test_latex_theme(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -261,7 +265,7 @@ def test_latex_theme(app, status, warning):
confoverrides={'latex_elements': {'papersize': 'b5paper',
'pointsize': '9pt'}})
def test_latex_theme_papersize(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -272,7 +276,7 @@ def test_latex_theme_papersize(app, status, warning):
confoverrides={'latex_theme_options': {'papersize': 'b5paper',
'pointsize': '9pt'}})
def test_latex_theme_options(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert r'\def\sphinxdocclass{book}' in result
@@ -281,7 +285,7 @@ def test_latex_theme_options(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'zh'})
def test_latex_additional_settings_for_language_code(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -291,7 +295,7 @@ def test_latex_additional_settings_for_language_code(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'el'})
def test_latex_additional_settings_for_greek(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -302,7 +306,7 @@ def test_latex_additional_settings_for_greek(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-title')
def test_latex_title_after_admonitions(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -313,7 +317,7 @@ def test_latex_title_after_admonitions(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic',
confoverrides={'release': '1.0_0'})
def test_latex_release(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -325,7 +329,7 @@ def test_latex_release(app, status, warning):
@pytest.mark.sphinx('latex', testroot='numfig',
confoverrides={'numfig': True})
def test_numref(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -367,7 +371,7 @@ def test_numref(app, status, warning):
'code-block': 'Code-%s',
'section': 'SECTION-%s'}})
def test_numref_with_prefix1(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -415,7 +419,7 @@ def test_numref_with_prefix1(app, status, warning):
'code-block': 'Code-%s | ',
'section': 'SECTION_%s_'}})
def test_numref_with_prefix2(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -491,7 +495,7 @@ def test_numref_with_language_ja(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-numfig')
def test_latex_obey_numfig_is_false(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage{sphinx}' in result
@@ -504,7 +508,7 @@ def test_latex_obey_numfig_is_false(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'numfig_secnum_depth': 0})
def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,nonumfigreset,mathnumfig]{sphinx}' in result
@@ -517,7 +521,7 @@ def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,numfigreset=2,mathnumfig]{sphinx}' in result
@@ -530,7 +534,7 @@ def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning):
'latex', testroot='latex-numfig',
confoverrides={'numfig': True, 'math_numfig': False})
def test_latex_obey_numfig_but_math_numfig_false(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8')
assert '\\usepackage[,numfigreset=1]{sphinx}' in result
@@ -543,7 +547,7 @@ def test_latex_obey_numfig_but_math_numfig_false(app, status, warning):
def test_latex_add_latex_package(app, status, warning):
app.add_latex_package('foo')
app.add_latex_package('bar', 'baz')
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
assert '\\usepackage{foo}' in result
assert '\\usepackage[baz]{bar}' in result
@@ -551,7 +555,7 @@ def test_latex_add_latex_package(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-babel')
def test_babel_with_no_language_settings(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -576,7 +580,7 @@ def test_babel_with_no_language_settings(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de'})
def test_babel_with_language_de(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -601,7 +605,7 @@ def test_babel_with_language_de(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'ru'})
def test_babel_with_language_ru(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -626,7 +630,7 @@ def test_babel_with_language_ru(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'tr'})
def test_babel_with_language_tr(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -651,7 +655,7 @@ def test_babel_with_language_tr(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'ja'})
def test_babel_with_language_ja(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -675,7 +679,7 @@ def test_babel_with_language_ja(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'unknown'})
def test_babel_with_unknown_language(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -702,7 +706,7 @@ def test_babel_with_unknown_language(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de', 'latex_engine': 'lualatex'})
def test_polyglossia_with_language_de(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -728,7 +732,7 @@ def test_polyglossia_with_language_de(app, status, warning):
'latex', testroot='latex-babel',
confoverrides={'language': 'de-1901', 'latex_engine': 'lualatex'})
def test_polyglossia_with_language_de_1901(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -752,7 +756,7 @@ def test_polyglossia_with_language_de_1901(app, status, warning):
@pytest.mark.sphinx('latex')
def test_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -781,7 +785,7 @@ def test_footnote(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -821,7 +825,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_footnote_referred_multiple_times(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -843,7 +847,7 @@ def test_footnote_referred_multiple_times(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'inline'})
def test_latex_show_urls_is_inline(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -866,29 +870,29 @@ def test_latex_show_urls_is_inline(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}\n') in result
- assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx} (http://sphinx\\sphinxhyphen{}doc.org/)' in result
+ assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx} (https://sphinx\\sphinxhyphen{}doc.org/)' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n'
'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} '
- '(http://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}\n'
+ assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}\n'
'\\sphinxAtStartPar\nDescription' in result)
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' in result)
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}\n'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}\n'
'\\sphinxAtStartPar\nDescription' in result)
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} '
- '(http://sphinx\\sphinxhyphen{}doc.org/)}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist} '
+ '(https://sphinx\\sphinxhyphen{}doc.org/)}'
'\n\\sphinxAtStartPar\nDescription') in result
assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -900,7 +904,7 @@ def test_latex_show_urls_is_inline(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'footnote'})
def test_latex_show_urls_is_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -922,9 +926,9 @@ def test_latex_show_urls_is_footnote(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}'
+ assert ('\\sphinxhref{https://sphinx-doc.org/}{Sphinx}'
'%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n\\end{footnote}') in result
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n\\end{footnote}') in result
assert ('Third footnote: %\n\\begin{footnote}[6]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[7]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
@@ -932,25 +936,25 @@ def test_latex_show_urls_is_footnote(app, status, warning):
'\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[8]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}'
+ assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}'
'%\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}'
'{URL in term}\\sphinxfootnotemark[10]}%\n'
'\\begin{footnotetext}[10]'
'\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n'
'\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[12]}%\n'
'\\begin{footnotetext}[12]'
'\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}'
'\\sphinxfootnotemark[11]}%\n'
'\\begin{footnotetext}[11]'
'\\sphinxAtStartFootnote\n'
- '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n'
+ '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n'
'\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -962,7 +966,7 @@ def test_latex_show_urls_is_footnote(app, status, warning):
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'no'})
def test_latex_show_urls_is_no(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -984,21 +988,21 @@ def test_latex_show_urls_is_no(app, status, warning):
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
- assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result
+ assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx}' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n'
'\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n'
'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result
assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n'
'Fourth\n%\n\\end{footnote}\n') in result
- assert '\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}\n'
+ assert '\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}' in result
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term}}\n'
'\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n'
'\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n'
'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces '
'\n\\sphinxAtStartPar\nDescription') in result
- assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}'
+ assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}}'
'\n\\sphinxAtStartPar\nDescription') in result
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
@@ -1009,7 +1013,7 @@ def test_latex_show_urls_is_no(app, status, warning):
@pytest.mark.sphinx(
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'footnote',
- 'rst_prolog': '.. |URL| replace:: `text <http://www.example.com/>`__'})
+ 'rst_prolog': '.. |URL| replace:: `text <https://www.example.com/>`__'})
def test_latex_show_urls_footnote_and_substitutions(app, status, warning):
# hyperlinks in substitutions should not effect to make footnotes (refs: #4784)
test_latex_show_urls_is_footnote(app, status, warning)
@@ -1017,7 +1021,7 @@ def test_latex_show_urls_footnote_and_substitutions(app, status, warning):
@pytest.mark.sphinx('latex', testroot='image-in-section')
def test_image_in_section(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1035,12 +1039,12 @@ def test_image_in_section(app, status, warning):
confoverrides={'latex_logo': 'notfound.jpg'})
def test_latex_logo_if_not_found(app, status, warning):
with pytest.raises(SphinxError):
- app.builder.build_all()
+ app.build(force_all=True)
@pytest.mark.sphinx('latex', testroot='toctree-maxdepth')
def test_toctree_maxdepth_manual(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1057,7 +1061,7 @@ def test_toctree_maxdepth_manual(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_toctree_maxdepth_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1071,7 +1075,7 @@ def test_toctree_maxdepth_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'foo'})
def test_toctree_not_found(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1085,7 +1089,7 @@ def test_toctree_not_found(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'bar'})
def test_toctree_without_maxdepth(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1098,7 +1102,7 @@ def test_toctree_without_maxdepth(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'root_doc': 'qux'})
def test_toctree_with_deeper_maxdepth(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1111,7 +1115,7 @@ def test_toctree_with_deeper_maxdepth(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': None})
def test_latex_toplevel_sectioning_is_None(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1123,7 +1127,7 @@ def test_latex_toplevel_sectioning_is_None(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'part'})
def test_latex_toplevel_sectioning_is_part(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1141,7 +1145,7 @@ def test_latex_toplevel_sectioning_is_part(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1155,7 +1159,7 @@ def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'chapter'})
def test_latex_toplevel_sectioning_is_chapter(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1171,7 +1175,7 @@ def test_latex_toplevel_sectioning_is_chapter(app, status, warning):
'Georg Brandl', 'howto'),
]})
def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1183,7 +1187,7 @@ def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning):
'latex', testroot='toctree-maxdepth',
confoverrides={'latex_toplevel_sectioning': 'section'})
def test_latex_toplevel_sectioning_is_section(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1194,7 +1198,7 @@ def test_latex_toplevel_sectioning_is_section(app, status, warning):
@skip_if_stylefiles_notfound
@pytest.mark.sphinx('latex', testroot='maxlistdepth')
def test_maxlistdepth_at_ten(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
print(status.getvalue())
@@ -1206,7 +1210,7 @@ def test_maxlistdepth_at_ten(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_tabulars(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|chapter){', result)[1:]:
@@ -1277,7 +1281,7 @@ def test_latex_table_tabulars(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_longtable(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|chapter){', result)[1:]:
@@ -1338,7 +1342,7 @@ def test_latex_table_longtable(app, status, warning):
confoverrides={'latex_table_style': []})
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_complex_tables(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
tables = {}
for chap in re.split(r'\\(?:section|renewcommand){', result)[1:]:
@@ -1368,7 +1372,7 @@ def test_latex_table_complex_tables(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table')
def test_latex_table_with_booktabs_and_colorrows(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\PassOptionsToPackage{booktabs}{sphinx}' in result
assert r'\PassOptionsToPackage{colorrows}{sphinx}' in result
@@ -1384,7 +1388,7 @@ def test_latex_table_with_booktabs_and_colorrows(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'templates_path': ['_mytemplates/latex']})
def test_latex_table_custom_template_caseA(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' in result
@@ -1392,7 +1396,7 @@ def test_latex_table_custom_template_caseA(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'templates_path': ['_mytemplates']})
def test_latex_table_custom_template_caseB(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' not in result
@@ -1400,14 +1404,14 @@ def test_latex_table_custom_template_caseB(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-table')
@pytest.mark.test_params(shared_result='latex-table')
def test_latex_table_custom_template_caseC(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert 'SALUT LES COPAINS' not in result
@pytest.mark.sphinx('latex', testroot='directives-raw')
def test_latex_raw_directive(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# standard case
@@ -1422,18 +1426,19 @@ def test_latex_raw_directive(app, status, warning):
@pytest.mark.sphinx('latex', testroot='images')
def test_latex_images(app, status, warning):
- app.builder.build_all()
+ with http_server(RemoteImageHandler, port=7777):
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# images are copied
- assert '\\sphinxincludegraphics{{python-logo}.png}' in result
- assert (app.outdir / 'python-logo.png').exists()
+ assert '\\sphinxincludegraphics{{sphinx}.png}' in result
+ assert (app.outdir / 'sphinx.png').exists()
# not found images
assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result
assert ('WARNING: Could not fetch remote image: '
- 'https://www.google.com/NOT_EXIST.PNG [404]' in warning.getvalue())
+ 'http://localhost:7777/NOT_EXIST.PNG [404]' in warning.getvalue())
# an image having target
assert ('\\sphinxhref{https://www.sphinx-doc.org/}'
@@ -1446,7 +1451,7 @@ def test_latex_images(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-index')
def test_latex_index(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('A \\index{famous@\\spxentry{famous}}famous '
@@ -1460,7 +1465,7 @@ def test_latex_index(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-equations')
def test_latex_equations(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
expected = (app.srcdir / 'expects' / 'latex-equations.tex').read_text(encoding='utf8').strip()
@@ -1470,7 +1475,7 @@ def test_latex_equations(app, status, warning):
@pytest.mark.sphinx('latex', testroot='image-in-parsed-literal')
def test_latex_image_in_parsed_literal(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('{\\sphinxunactivateextrasandspace \\raisebox{-0.5\\height}'
@@ -1480,7 +1485,7 @@ def test_latex_image_in_parsed_literal(app, status, warning):
@pytest.mark.sphinx('latex', testroot='nested-enumerated-list')
def test_latex_nested_enumerated_list(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n'
@@ -1497,7 +1502,7 @@ def test_latex_nested_enumerated_list(app, status, warning):
@pytest.mark.sphinx('latex', testroot='footnotes')
def test_latex_thebibliography(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
@@ -1510,7 +1515,7 @@ def test_latex_thebibliography(app, status, warning):
@pytest.mark.sphinx('latex', testroot='glossary')
def test_latex_glossary(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert (r'\sphinxlineitem{ähnlich\index{ähnlich@\spxentry{ähnlich}|spxpagem}'
@@ -1534,7 +1539,7 @@ def test_latex_glossary(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-labels')
def test_latex_labels(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
@@ -1583,7 +1588,7 @@ def test_latex_labels(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-figure-in-admonition')
def test_latex_figure_in_admonition(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\begin{figure}[H]' in result
@@ -1594,7 +1599,6 @@ def test_default_latex_documents():
config = Config({'root_doc': 'index',
'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein."})
- config.init_values()
config.add('latex_engine', None, True, None)
config.add('latex_theme', 'manual', True, None)
expected = [('index', 'stasi.tex', 'STASI™ Documentation',
@@ -1606,7 +1610,7 @@ def test_default_latex_documents():
@skip_if_stylefiles_notfound
@pytest.mark.sphinx('latex', testroot='latex-includegraphics')
def test_includegraphics_oversized(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
print(status.getvalue())
print(warning.getvalue())
compile_latex_document(app)
@@ -1614,7 +1618,7 @@ def test_includegraphics_oversized(app, status, warning):
@pytest.mark.sphinx('latex', testroot='index_on_title')
def test_index_on_title(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert ('\\chapter{Test for index in top level title}\n'
'\\label{\\detokenize{contents:test-for-index-in-top-level-title}}'
@@ -1625,7 +1629,7 @@ def test_index_on_title(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'pdflatex'})
def test_texescape_for_non_unicode_supported_engine(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert 'script small e: e' in result
@@ -1637,7 +1641,7 @@ def test_texescape_for_non_unicode_supported_engine(app, status, warning):
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'xelatex'})
def test_texescape_for_unicode_supported_engine(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
print(result)
assert 'script small e: e' in result
@@ -1649,20 +1653,20 @@ def test_texescape_for_unicode_supported_engine(app, status, warning):
@pytest.mark.sphinx('latex', testroot='basic',
confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}})
def test_latex_elements_extrapackages(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'test.tex').read_text(encoding='utf8')
assert r'\usepackage{foo}' in result
@pytest.mark.sphinx('latex', testroot='nested-tables')
def test_latex_nested_tables(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert warning.getvalue() == ''
@pytest.mark.sphinx('latex', testroot='latex-container')
def test_latex_container(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
assert r'\begin{sphinxuseclass}{classname}' in result
assert r'\end{sphinxuseclass}' in result
@@ -1704,7 +1708,7 @@ def test_copy_images(app, status, warning):
image.name for image in test_dir.rglob('*')
if image.suffix in {'.gif', '.pdf', '.png', '.svg'}
}
- images.discard('python-logo.png')
+ images.discard('sphinx.png')
assert images == {
'img.pdf',
'rimg.png',
@@ -1745,7 +1749,7 @@ def test_duplicated_labels_before_module(app, status, warning):
@pytest.mark.sphinx('latex', testroot='domain-py-python_maximum_signature_line_length',
confoverrides={'python_maximum_signature_line_length': 23})
def test_one_parameter_per_line(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'python.tex').read_text(encoding='utf8')
# TODO: should these asserts check presence or absence of a final \sphinxparamcomma?
diff --git a/tests/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py
index 38a0bd1..c8d8515 100644
--- a/tests/test_build_linkcheck.py
+++ b/tests/test_builders/test_build_linkcheck.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import http.server
import json
import re
import sys
@@ -10,10 +9,11 @@ import textwrap
import time
import wsgiref.handlers
from base64 import b64encode
-from os import path
+from http.server import BaseHTTPRequestHandler
from queue import Queue
from unittest import mock
+import docutils
import pytest
from urllib3.poolmanager import PoolManager
@@ -23,18 +23,18 @@ from sphinx.builders.linkcheck import (
Hyperlink,
HyperlinkAvailabilityCheckWorker,
RateLimit,
+ compile_linkcheck_allowed_redirects,
)
-from sphinx.testing.util import strip_escseq
+from sphinx.deprecation import RemovedInSphinx80Warning
from sphinx.util import requests
from sphinx.util.console import strip_colors
-from .utils import CERT_FILE, http_server, https_server
+from tests.utils import CERT_FILE, serve_application
ts_re = re.compile(r".*\[(?P<ts>.*)\].*")
-SPHINX_DOCS_INDEX = path.abspath(path.join(__file__, "..", "roots", "test-linkcheck", "sphinx-docs-index.html"))
-class DefaultsHandler(http.server.BaseHTTPRequestHandler):
+class DefaultsHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -102,7 +102,7 @@ class ConnectionMeasurement:
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
def test_defaults(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
with ConnectionMeasurement() as m:
app.build()
assert m.connection_count <= 5
@@ -115,9 +115,9 @@ def test_defaults(app):
assert "Anchor 'top' not found" in content
assert "Anchor 'does-not-exist' not found" in content
# images should fail
- assert "Not Found for url: http://localhost:7777/image.png" in content
- assert "Not Found for url: http://localhost:7777/image2.png" in content
- # looking for local file should fail
+ assert f"Not Found for url: http://{address}/image.png" in content
+ assert f"Not Found for url: http://{address}/image2.png" in content
+ # looking for missing local file should fail
assert "[broken] path/to/notfound" in content
assert len(content.splitlines()) == 5
@@ -135,35 +135,42 @@ def test_defaults(app):
# the output order of the rows is not stable
# due to possible variance in network latency
rowsby = {row["uri"]: row for row in rows}
- assert rowsby["http://localhost:7777#!bar"] == {
+ # looking for local file that exists should succeed
+ assert rowsby["conf.py"]["status"] == "working"
+ assert rowsby[f"http://{address}#!bar"] == {
'filename': 'links.rst',
'lineno': 5,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777#!bar',
+ 'uri': f'http://{address}#!bar',
'info': '',
}
- assert rowsby['http://localhost:7777/image2.png'] == {
- 'filename': 'links.rst',
- 'lineno': 13,
- 'status': 'broken',
- 'code': 0,
- 'uri': 'http://localhost:7777/image2.png',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/image2.png',
- }
+
+ def _missing_resource(filename: str, lineno: int):
+ return {
+ 'filename': 'links.rst',
+ 'lineno': lineno,
+ 'status': 'broken',
+ 'code': 0,
+ 'uri': f'http://{address}/{filename}',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/{filename}',
+ }
+ accurate_linenumbers = docutils.__version_info__[:2] >= (0, 21)
+ image2_lineno = 12 if accurate_linenumbers else 13
+ assert rowsby[f'http://{address}/image2.png'] == _missing_resource("image2.png", image2_lineno)
# looking for '#top' and '#does-not-exist' not found should fail
- assert rowsby["http://localhost:7777/#top"]["info"] == "Anchor 'top' not found"
- assert rowsby["http://localhost:7777/#top"]["status"] == "broken"
- assert rowsby["http://localhost:7777#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found"
+ assert rowsby[f"http://{address}/#top"]["info"] == "Anchor 'top' not found"
+ assert rowsby[f"http://{address}/#top"]["status"] == "broken"
+ assert rowsby[f"http://{address}#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found"
# images should fail
- assert "Not Found for url: http://localhost:7777/image.png" in rowsby["http://localhost:7777/image.png"]["info"]
+ assert f"Not Found for url: http://{address}/image.png" in rowsby[f"http://{address}/image.png"]["info"]
# anchor should be found
- assert rowsby['http://localhost:7777/anchor.html#found'] == {
+ assert rowsby[f'http://{address}/anchor.html#found'] == {
'filename': 'links.rst',
'lineno': 14,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777/anchor.html#found',
+ 'uri': f'http://{address}/anchor.html#found',
'info': '',
}
@@ -172,7 +179,7 @@ def test_defaults(app):
'linkcheck', testroot='linkcheck', freshenv=True,
confoverrides={'linkcheck_anchors': False})
def test_check_link_response_only(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
app.build()
# JSON output
@@ -181,12 +188,12 @@ def test_check_link_response_only(app):
rows = [json.loads(x) for x in content.splitlines()]
rowsby = {row["uri"]: row for row in rows}
- assert rowsby["http://localhost:7777/#top"]["status"] == "working"
+ assert rowsby[f"http://{address}/#top"]["status"] == "working"
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True)
def test_too_many_retries(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler) as address:
app.build()
# Text output
@@ -210,12 +217,20 @@ def test_too_many_retries(app):
assert row['lineno'] == 1
assert row['status'] == 'broken'
assert row['code'] == 0
- assert row['uri'] == 'https://localhost:7777/doesnotexist'
+ assert row['uri'] == f'https://{address}/doesnotexist'
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True)
def test_raw_node(app):
- with http_server(OKHandler):
+ with serve_application(app, OKHandler) as address:
+ # write an index file that contains a link back to this webserver's root
+ # URL. docutils will replace the raw node with the contents retrieved..
+ # ..and then the linkchecker will check that the root URL is available.
+ index = (app.srcdir / "index.rst")
+ index.write_text(
+ ".. raw:: 'html'\n"
+ " :url: http://{address}/".format(address=address),
+ )
app.build()
# JSON output
@@ -231,7 +246,7 @@ def test_raw_node(app):
'lineno': 1,
'status': 'working',
'code': 0,
- 'uri': 'http://localhost:7777/',
+ 'uri': f'http://{address}/', # the received rST contains a link to its' own URL
'info': '',
}
@@ -240,7 +255,7 @@ def test_raw_node(app):
'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True,
confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]})
def test_anchors_ignored(app):
- with http_server(OKHandler):
+ with serve_application(app, OKHandler):
app.build()
assert (app.outdir / 'output.txt').exists()
@@ -250,7 +265,7 @@ def test_anchors_ignored(app):
assert not content
-class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler):
+class AnchorsIgnoreForUrlHandler(BaseHTTPRequestHandler):
def do_HEAD(self):
if self.path in {'/valid', '/ignored'}:
self.send_response(200, "OK")
@@ -266,14 +281,13 @@ class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(b"no anchor but page exists\n")
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True,
- confoverrides={'linkcheck_anchors_ignore_for_url': [
- 'http://localhost:7777/ignored', # existing page
- 'http://localhost:7777/invalid', # unknown page
- ]})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True)
def test_anchors_ignored_for_url(app):
- with http_server(AnchorsIgnoreForUrlHandler):
+ with serve_application(app, AnchorsIgnoreForUrlHandler) as address:
+ app.config.linkcheck_anchors_ignore_for_url = [ # type: ignore[attr-defined]
+ f'http://{address}/ignored', # existing page
+ f'http://{address}/invalid', # unknown page
+ ]
app.build()
assert (app.outdir / 'output.txt').exists()
@@ -288,41 +302,41 @@ def test_anchors_ignored_for_url(app):
# the order the threads are processing the links
rows = {r['uri']: {'status': r['status'], 'info': r['info']} for r in data}
- assert rows['http://localhost:7777/valid']['status'] == 'working'
- assert rows['http://localhost:7777/valid#valid-anchor']['status'] == 'working'
- assert rows['http://localhost:7777/valid#invalid-anchor'] == {
+ assert rows[f'http://{address}/valid']['status'] == 'working'
+ assert rows[f'http://{address}/valid#valid-anchor']['status'] == 'working'
+ assert rows[f'http://{address}/valid#invalid-anchor'] == {
'status': 'broken',
'info': "Anchor 'invalid-anchor' not found",
}
- assert rows['http://localhost:7777/ignored']['status'] == 'working'
- assert rows['http://localhost:7777/ignored#invalid-anchor']['status'] == 'working'
+ assert rows[f'http://{address}/ignored']['status'] == 'working'
+ assert rows[f'http://{address}/ignored#invalid-anchor']['status'] == 'working'
- assert rows['http://localhost:7777/invalid'] == {
+ assert rows[f'http://{address}/invalid'] == {
'status': 'broken',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/invalid',
}
- assert rows['http://localhost:7777/invalid#anchor'] == {
+ assert rows[f'http://{address}/invalid#anchor'] == {
'status': 'broken',
- 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid',
+ 'info': f'404 Client Error: Not Found for url: http://{address}/invalid',
}
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True)
def test_raises_for_invalid_status(app):
- class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler):
+ class InternalServerErrorHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
self.send_error(500, "Internal Server Error")
- with http_server(InternalServerErrorHandler):
+ with serve_application(app, InternalServerErrorHandler) as address:
app.build()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
- "index.rst:1: [broken] http://localhost:7777/#anchor: "
+ f"index.rst:1: [broken] http://{address}/#anchor: "
"500 Server Error: Internal Server Error "
- "for url: http://localhost:7777/\n"
+ f"for url: http://{address}/\n"
)
@@ -338,13 +352,17 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True):
expected_token = b64encode(":".join(valid_credentials).encode()).decode("utf-8")
del valid_credentials
- class CustomHandler(http.server.BaseHTTPRequestHandler):
+ class CustomHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def authenticated(method):
def method_if_authenticated(self):
- if (expected_token is None
- or self.headers["Authorization"] == f"Basic {expected_token}"):
+ if expected_token is None:
+ return method(self)
+ elif not self.headers["Authorization"]:
+ self.send_response(401, "Unauthorized")
+ self.end_headers()
+ elif self.headers["Authorization"] == f"Basic {expected_token}":
return method(self)
else:
self.send_response(403, "Forbidden")
@@ -370,15 +388,14 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True):
return CustomHandler
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_auth': [
- (r'^$', ('no', 'match')),
- (r'^http://localhost:7777/$', ('user1', 'password')),
- (r'.*local.*', ('user2', 'hunter2')),
- ]})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_auth_header_uses_first_match(app):
- with http_server(custom_handler(valid_credentials=("user1", "password"))):
+ with serve_application(app, custom_handler(valid_credentials=("user1", "password"))) as address:
+ app.config.linkcheck_auth = [ # type: ignore[attr-defined]
+ (r'^$', ('no', 'match')),
+ (fr'^http://{re.escape(address)}/$', ('user1', 'password')),
+ (r'.*local.*', ('user2', 'hunter2')),
+ ]
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -387,41 +404,51 @@ def test_auth_header_uses_first_match(app):
assert content["status"] == "working"
+@pytest.mark.filterwarnings('ignore::sphinx.deprecation.RemovedInSphinx80Warning')
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
-def test_auth_header_no_match(app):
- with http_server(custom_handler(valid_credentials=("user1", "password"))):
+ confoverrides={'linkcheck_allow_unauthorized': False})
+def test_unauthorized_broken(app):
+ with serve_application(app, custom_handler(valid_credentials=("user1", "password"))):
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
content = json.load(fp)
- # TODO: should this test's webserver return HTTP 401 here?
- # https://github.com/sphinx-doc/sphinx/issues/11433
- assert content["info"] == "403 Client Error: Forbidden for url: http://localhost:7777/"
+ assert content["info"] == "unauthorized"
assert content["status"] == "broken"
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_request_headers': {
- "http://localhost:7777/": {
- "Accept": "text/html",
- },
- "*": {
- "X-Secret": "open sesami",
- },
- }})
+ confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
+def test_auth_header_no_match(app):
+ with (
+ serve_application(app, custom_handler(valid_credentials=("user1", "password"))),
+ pytest.warns(RemovedInSphinx80Warning, match='linkcheck builder encountered an HTTP 401'),
+ ):
+ app.build()
+
+ with open(app.outdir / "output.json", encoding="utf-8") as fp:
+ content = json.load(fp)
+
+ # This link is considered working based on the default linkcheck_allow_unauthorized=true
+ assert content["info"] == "unauthorized"
+ assert content["status"] == "working"
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_linkcheck_request_headers(app):
def check_headers(self):
if "X-Secret" in self.headers:
return False
- if self.headers["Accept"] != "text/html":
- return False
- return True
+ return self.headers["Accept"] == "text/html"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
+ app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
+ f"http://{address}/": {"Accept": "text/html"},
+ "*": {"X-Secret": "open sesami"},
+ }
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -430,21 +457,18 @@ def test_linkcheck_request_headers(app):
assert content["status"] == "working"
-@pytest.mark.sphinx(
- 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
- confoverrides={'linkcheck_request_headers': {
- "http://localhost:7777": {"Accept": "application/json"},
- "*": {"X-Secret": "open sesami"},
- }})
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_linkcheck_request_headers_no_slash(app):
def check_headers(self):
if "X-Secret" in self.headers:
return False
- if self.headers["Accept"] != "application/json":
- return False
- return True
+ return self.headers["Accept"] == "application/json"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
+ app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
+ f"http://{address}": {"Accept": "application/json"},
+ "*": {"X-Secret": "open sesami"},
+ }
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -463,11 +487,9 @@ def test_linkcheck_request_headers_default(app):
def check_headers(self):
if self.headers["X-Secret"] != "open sesami":
return False
- if self.headers["Accept"] == "application/json":
- return False
- return True
+ return self.headers["Accept"] != "application/json"
- with http_server(custom_handler(success_criteria=check_headers)):
+ with serve_application(app, custom_handler(success_criteria=check_headers)):
app.build()
with open(app.outdir / "output.json", encoding="utf-8") as fp:
@@ -477,7 +499,7 @@ def test_linkcheck_request_headers_default(app):
def make_redirect_handler(*, support_head):
- class RedirectOnceHandler(http.server.BaseHTTPRequestHandler):
+ class RedirectOnceHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -493,7 +515,7 @@ def make_redirect_handler(*, support_head):
self.send_response(204, "No content")
else:
self.send_response(302, "Found")
- self.send_header("Location", "http://localhost:7777/?redirected=1")
+ self.send_header("Location", "/?redirected=1")
self.send_header("Content-Length", "0")
self.end_headers()
@@ -506,13 +528,13 @@ def make_redirect_handler(*, support_head):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_follows_redirects_on_HEAD(app, capsys, warning):
- with http_server(make_redirect_handler(support_head=True)):
+ with serve_application(app, make_redirect_handler(support_head=True)) as address:
app.build()
stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
"index.rst:1: [redirected with Found] "
- "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ f"http://{address}/ to http://{address}/?redirected=1\n"
)
assert stderr == textwrap.dedent(
"""\
@@ -525,13 +547,13 @@ def test_follows_redirects_on_HEAD(app, capsys, warning):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_follows_redirects_on_GET(app, capsys, warning):
- with http_server(make_redirect_handler(support_head=False)):
+ with serve_application(app, make_redirect_handler(support_head=False)) as address:
app.build()
stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
"index.rst:1: [redirected with Found] "
- "http://localhost:7777/ to http://localhost:7777/?redirected=1\n"
+ f"http://{address}/ to http://{address}/?redirected=1\n"
)
assert stderr == textwrap.dedent(
"""\
@@ -543,35 +565,34 @@ def test_follows_redirects_on_GET(app, capsys, warning):
assert warning.getvalue() == ''
-@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects',
- freshenv=True, confoverrides={
- 'linkcheck_allowed_redirects': {'http://localhost:7777/.*1': '.*'},
- })
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects')
def test_linkcheck_allowed_redirects(app, warning):
- with http_server(make_redirect_handler(support_head=False)):
+ with serve_application(app, make_redirect_handler(support_head=False)) as address:
+ app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} # type: ignore[attr-defined]
+ compile_linkcheck_allowed_redirects(app, app.config)
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
- rows = [json.loads(l) for l in fp.readlines()]
+ rows = [json.loads(l) for l in fp]
assert len(rows) == 2
records = {row["uri"]: row for row in rows}
- assert records["http://localhost:7777/path1"]["status"] == "working"
- assert records["http://localhost:7777/path2"] == {
+ assert records[f"http://{address}/path1"]["status"] == "working"
+ assert records[f"http://{address}/path2"] == {
'filename': 'index.rst',
'lineno': 3,
'status': 'redirected',
'code': 302,
- 'uri': 'http://localhost:7777/path2',
- 'info': 'http://localhost:7777/?redirected=1',
+ 'uri': f'http://{address}/path2',
+ 'info': f'http://{address}/?redirected=1',
}
- assert ("index.rst:3: WARNING: redirect http://localhost:7777/path2 - with Found to "
- "http://localhost:7777/?redirected=1\n" in strip_escseq(warning.getvalue()))
+ assert (f"index.rst:3: WARNING: redirect http://{address}/path2 - with Found to "
+ f"http://{address}/?redirected=1\n" in strip_colors(warning.getvalue()))
assert len(warning.getvalue().splitlines()) == 1
-class OKHandler(http.server.BaseHTTPRequestHandler):
+class OKHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -591,7 +612,7 @@ class OKHandler(http.server.BaseHTTPRequestHandler):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_invalid_ssl(get_request, app):
# Link indicates SSL should be used (https) but the server does not handle it.
- with http_server(OKHandler):
+ with serve_application(app, OKHandler) as address:
app.build()
assert not get_request.called
@@ -600,13 +621,13 @@ def test_invalid_ssl(get_request, app):
assert content["status"] == "broken"
assert content["filename"] == "index.rst"
assert content["lineno"] == 1
- assert content["uri"] == "https://localhost:7777/"
+ assert content["uri"] == f"https://{address}/"
assert "SSLError" in content["info"]
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_fails(app):
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -614,14 +635,14 @@ def test_connect_to_selfsigned_fails(app):
assert content["status"] == "broken"
assert content["filename"] == "index.rst"
assert content["lineno"] == 1
- assert content["uri"] == "https://localhost:7777/"
+ assert content["uri"] == f"https://{address}/"
assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"]
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_tls_verify_false(app):
app.config.tls_verify = False
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -631,7 +652,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -639,7 +660,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_tls_cacerts(app):
app.config.tls_cacerts = CERT_FILE
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -649,7 +670,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -657,7 +678,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
monkeypatch.setenv("REQUESTS_CA_BUNDLE", CERT_FILE)
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -667,7 +688,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "",
}
@@ -675,7 +696,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_nonexistent_cert_file(app):
app.config.tls_cacerts = "does/not/exist"
- with https_server(OKHandler):
+ with serve_application(app, OKHandler, tls_enabled=True) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -685,17 +706,17 @@ def test_connect_to_selfsigned_nonexistent_cert_file(app):
"status": "broken",
"filename": "index.rst",
"lineno": 1,
- "uri": "https://localhost:7777/",
+ "uri": f'https://{address}/',
"info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist",
}
-class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler):
+class InfiniteRedirectOnHeadHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
self.send_response(302, "Found")
- self.send_header("Location", "http://localhost:7777/")
+ self.send_header("Location", "/")
self.send_header("Content-Length", "0")
self.end_headers()
@@ -714,7 +735,7 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch):
monkeypatch.setattr(requests.sessions, "DEFAULT_REDIRECT_LIMIT", 5)
- with http_server(InfiniteRedirectOnHeadHandler):
+ with serve_application(app, InfiniteRedirectOnHeadHandler) as address:
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
@@ -724,13 +745,13 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch):
"status": "working",
"filename": "index.rst",
"lineno": 1,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
def make_retry_after_handler(responses):
- class RetryAfterHandler(http.server.BaseHTTPRequestHandler):
+ class RetryAfterHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -750,9 +771,11 @@ def make_retry_after_handler(responses):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_retry_after_int_delay(app, capsys, status):
- with http_server(make_retry_after_handler([(429, "0"), (200, None)])), \
- mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), \
- mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01):
+ with (
+ serve_application(app, make_retry_after_handler([(429, "0"), (200, None)])) as address,
+ mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0),
+ mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01),
+ ):
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -760,10 +783,10 @@ def test_too_many_requests_retry_after_int_delay(app, capsys, status):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
- rate_limit_log = "-rate limited- http://localhost:7777/ | sleeping...\n"
+ rate_limit_log = f"-rate limited- http://{address}/ | sleeping...\n"
assert rate_limit_log in strip_colors(status.getvalue())
_stdout, stderr = capsys.readouterr()
assert stderr == textwrap.dedent(
@@ -787,7 +810,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
m.setattr(sphinx.util.http_date, '_GMT_OFFSET',
float(time.localtime().tm_gmtoff))
- with http_server(make_retry_after_handler([(429, retry_after), (200, None)])):
+ with serve_application(app, make_retry_after_handler([(429, retry_after), (200, None)])) as address:
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
@@ -796,7 +819,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
_stdout, stderr = capsys.readouterr()
@@ -810,8 +833,10 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_retry_after_without_header(app, capsys):
- with http_server(make_retry_after_handler([(429, None), (200, None)])), \
- mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0):
+ with (
+ serve_application(app, make_retry_after_handler([(429, None), (200, None)])) as address,
+ mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0),
+ ):
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -819,7 +844,7 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
_stdout, stderr = capsys.readouterr()
@@ -831,10 +856,36 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
)
+@pytest.mark.sphinx(
+ 'linkcheck', testroot='linkcheck-localserver', freshenv=True,
+ confoverrides={
+ 'linkcheck_report_timeouts_as_broken': False,
+ 'linkcheck_timeout': 0.01,
+ }
+)
+def test_requests_timeout(app):
+ class DelayedResponseHandler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ time.sleep(0.2) # wait before sending any response data
+ self.send_response(200, "OK")
+ self.send_header("Content-Length", "0")
+ self.end_headers()
+
+ with serve_application(app, DelayedResponseHandler):
+ app.build()
+
+ with open(app.outdir / "output.json", encoding="utf-8") as fp:
+ content = json.load(fp)
+
+ assert content["status"] == "timeout"
+
+
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_user_timeout(app):
app.config.linkcheck_rate_limit_timeout = 0.0
- with http_server(make_retry_after_handler([(429, None)])):
+ with serve_application(app, make_retry_after_handler([(429, None)])) as address:
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
assert json.loads(content) == {
@@ -842,8 +893,8 @@ def test_too_many_requests_user_timeout(app):
"lineno": 1,
"status": "broken",
"code": 0,
- "uri": "http://localhost:7777/",
- "info": "429 Client Error: Too Many Requests for url: http://localhost:7777/",
+ "uri": f'http://{address}/',
+ "info": f"429 Client Error: Too Many Requests for url: http://{address}/",
}
@@ -901,14 +952,15 @@ def test_connection_contention(get_adapter, app, capsys):
import socket
socket.setdefaulttimeout(5)
- # Place a workload into the linkcheck queue
- link_count = 10
- rqueue, wqueue = Queue(), Queue()
- for _ in range(link_count):
- wqueue.put(CheckRequest(0, Hyperlink("http://localhost:7777", "test", "test.rst", 1)))
-
# Create parallel consumer threads
- with http_server(make_redirect_handler(support_head=True)):
+ with serve_application(app, make_redirect_handler(support_head=True)) as address:
+
+ # Place a workload into the linkcheck queue
+ link_count = 10
+ rqueue, wqueue = Queue(), Queue()
+ for _ in range(link_count):
+ wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", "test.rst", 1)))
+
begin, checked = time.time(), []
threads = [
HyperlinkAvailabilityCheckWorker(
@@ -932,7 +984,7 @@ def test_connection_contention(get_adapter, app, capsys):
assert "TimeoutError" not in stderr
-class ConnectionResetHandler(http.server.BaseHTTPRequestHandler):
+class ConnectionResetHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_HEAD(self):
@@ -946,7 +998,7 @@ class ConnectionResetHandler(http.server.BaseHTTPRequestHandler):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_get_after_head_raises_connection_error(app):
- with http_server(ConnectionResetHandler):
+ with serve_application(app, ConnectionResetHandler) as address:
app.build()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert not content
@@ -956,34 +1008,33 @@ def test_get_after_head_raises_connection_error(app):
"lineno": 1,
"status": "working",
"code": 0,
- "uri": "http://localhost:7777/",
+ "uri": f'http://{address}/',
"info": "",
}
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-documents_exclude', freshenv=True)
def test_linkcheck_exclude_documents(app):
- with http_server(DefaultsHandler):
+ with serve_application(app, DefaultsHandler):
app.build()
with open(app.outdir / 'output.json', encoding='utf-8') as fp:
content = [json.loads(record) for record in fp]
- assert content == [
- {
- 'filename': 'broken_link.rst',
- 'lineno': 4,
- 'status': 'ignored',
- 'code': 0,
- 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link',
- 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents',
- },
- {
- 'filename': 'br0ken_link.rst',
- 'lineno': 4,
- 'status': 'ignored',
- 'code': 0,
- 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link',
- 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents',
- },
- ]
+ assert len(content) == 2
+ assert {
+ 'filename': 'broken_link.rst',
+ 'lineno': 4,
+ 'status': 'ignored',
+ 'code': 0,
+ 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link',
+ 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents',
+ } in content
+ assert {
+ 'filename': 'br0ken_link.rst',
+ 'lineno': 4,
+ 'status': 'ignored',
+ 'code': 0,
+ 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link',
+ 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents',
+ } in content
diff --git a/tests/test_build_manpage.py b/tests/test_builders/test_build_manpage.py
index e765644..7172281 100644
--- a/tests/test_build_manpage.py
+++ b/tests/test_builders/test_build_manpage.py
@@ -9,7 +9,7 @@ from sphinx.config import Config
@pytest.mark.sphinx('man')
def test_all(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
assert (app.outdir / 'sphinxtests.1').exists()
content = (app.outdir / 'sphinxtests.1').read_text(encoding='utf8')
@@ -34,7 +34,7 @@ def test_all(app, status, warning):
@pytest.mark.sphinx('man', testroot='basic',
confoverrides={'man_pages': [('index', 'title', None, [], 1)]})
def test_man_pages_empty_description(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'title.1').read_text(encoding='utf8')
assert r'title \-' not in content
@@ -49,7 +49,7 @@ def test_man_make_section_directory(app, status, warning):
@pytest.mark.sphinx('man', testroot='directive-code')
def test_captioned_code_block(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
content = (app.outdir / 'python.1').read_text(encoding='utf8')
if docutils.__version_info__[:2] < (0, 21):
@@ -92,7 +92,6 @@ def test_default_man_pages():
config = Config({'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein",
'release': '1.0'})
- config.init_values()
expected = [('index', 'stasi', 'STASI™ Documentation 1.0',
["Wolfgang Schäuble & G'Beckstein"], 1)]
assert default_man_pages(config) == expected
diff --git a/tests/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py
index 9964382..f9effb2 100644
--- a/tests/test_build_texinfo.py
+++ b/tests/test_builders/test_build_texinfo.py
@@ -1,6 +1,5 @@
"""Test the build process with Texinfo builder with the test root."""
-import os
import re
import subprocess
from pathlib import Path
@@ -11,37 +10,14 @@ import pytest
from sphinx.builders.texinfo import default_texinfo_documents
from sphinx.config import Config
-from sphinx.testing.util import strip_escseq
from sphinx.util.docutils import new_document
from sphinx.writers.texinfo import TexinfoTranslator
-from .test_build_html import ENV_WARNINGS
-
-TEXINFO_WARNINGS = ENV_WARNINGS + """\
-%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
-%(root)s/index.rst:\\d+: WARNING: citation not found: missing
-%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\*
-%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \
-\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\)
-"""
-
-
-@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True)
-def test_texinfo_warnings(app, status, warning):
- app.builder.build_all()
- warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
- warnings_exp = TEXINFO_WARNINGS % {
- 'root': re.escape(app.srcdir.as_posix())}
- assert re.match(warnings_exp + '$', warnings), \
- "Warnings don't match:\n" + \
- '--- Expected (regex):\n' + warnings_exp + \
- '--- Got:\n' + warnings
-
@pytest.mark.sphinx('texinfo')
def test_texinfo(app, status, warning):
TexinfoTranslator.ignore_missing_images = True
- app.builder.build_all()
+ app.build(force_all=True)
result = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert ('@anchor{markup doc}@anchor{11}'
'@anchor{markup id1}@anchor{12}'
@@ -71,7 +47,7 @@ def test_texinfo_rubric(app, status, warning):
@pytest.mark.sphinx('texinfo', testroot='markup-citation')
def test_texinfo_citation(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'python.texi').read_text(encoding='utf8')
assert 'This is a citation ref; @ref{1,,[CITE1]} and @ref{2,,[CITE2]}.' in output
@@ -84,7 +60,6 @@ def test_texinfo_citation(app, status, warning):
def test_default_texinfo_documents():
config = Config({'project': 'STASI™ Documentation',
'author': "Wolfgang Schäuble & G'Beckstein"})
- config.init_values()
expected = [('index', 'stasi', 'STASI™ Documentation',
"Wolfgang Schäuble & G'Beckstein", 'stasi',
'One line description of project', 'Miscellaneous')]
@@ -110,7 +85,7 @@ def test_texinfo_escape_id(app, status, warning):
@pytest.mark.sphinx('texinfo', testroot='footnotes')
def test_texinfo_footnote(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'python.texi').read_text(encoding='utf8')
assert 'First footnote: @footnote{\nFirst\n}' in output
@@ -118,13 +93,13 @@ def test_texinfo_footnote(app, status, warning):
@pytest.mark.sphinx('texinfo')
def test_texinfo_xrefs(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert re.search(r'@ref{\w+,,--plugin\.option}', output)
# Now rebuild it without xrefs
app.config.texinfo_cross_references = False
- app.builder.build_all()
+ app.build(force_all=True)
output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8')
assert not re.search(r'@ref{\w+,,--plugin\.option}', output)
assert 'Link to perl +p, --ObjC++, --plugin.option, create-auth-token, arg and -j' in output
diff --git a/tests/test_build_text.py b/tests/test_builders/test_build_text.py
index 4a53be3..6dc0d03 100644
--- a/tests/test_build_text.py
+++ b/tests/test_builders/test_build_text.py
@@ -17,7 +17,7 @@ def with_text_app(*args, **kw):
@with_text_app()
def test_maxwitdh_with_prefix(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'maxwidth.txt').read_text(encoding='utf8')
lines = result.splitlines()
@@ -40,7 +40,7 @@ def test_maxwitdh_with_prefix(app, status, warning):
@with_text_app()
def test_lineblock(app, status, warning):
# regression test for #1109: need empty line after line block
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'lineblock.txt').read_text(encoding='utf8')
expect = (
"* one\n"
@@ -55,7 +55,7 @@ def test_lineblock(app, status, warning):
@with_text_app()
def test_nonascii_title_line(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_title.txt').read_text(encoding='utf8')
expect_underline = '*********'
result_underline = result.splitlines()[1].strip()
@@ -64,7 +64,7 @@ def test_nonascii_title_line(app, status, warning):
@with_text_app()
def test_nonascii_table(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_table.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
line_widths = [column_width(line) for line in lines]
@@ -73,7 +73,7 @@ def test_nonascii_table(app, status, warning):
@with_text_app()
def test_nonascii_maxwidth(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'nonascii_maxwidth.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
line_widths = [column_width(line) for line in lines]
@@ -117,7 +117,7 @@ def test_table_cell():
@with_text_app()
def test_table_with_empty_cell(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -131,7 +131,7 @@ def test_table_with_empty_cell(app, status, warning):
@with_text_app()
def test_table_with_rowspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_rowspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -145,7 +145,7 @@ def test_table_with_rowspan(app, status, warning):
@with_text_app()
def test_table_with_colspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -159,7 +159,7 @@ def test_table_with_colspan(app, status, warning):
@with_text_app()
def test_table_with_colspan_left(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan_left.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert lines[0] == "+-------+-------+"
@@ -173,7 +173,7 @@ def test_table_with_colspan_left(app, status, warning):
@with_text_app()
def test_table_with_colspan_and_rowspan(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'table_colspan_and_rowspan.txt').read_text(encoding='utf8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
assert result
@@ -188,7 +188,7 @@ def test_table_with_colspan_and_rowspan(app, status, warning):
@with_text_app()
def test_list_items_in_admonition(app, status, warning):
- app.builder.build_update()
+ app.build()
result = (app.outdir / 'listitems.txt').read_text(encoding='utf8')
lines = [line.rstrip() for line in result.splitlines()]
assert lines[0] == "See also:"
@@ -200,7 +200,7 @@ def test_list_items_in_admonition(app, status, warning):
@with_text_app()
def test_secnums(app, status, warning):
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* 1. Section A"
@@ -226,7 +226,7 @@ def test_secnums(app, status, warning):
assert doc2 == expect
app.config.text_secnumber_suffix = " "
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* 1 Section A"
@@ -252,7 +252,7 @@ def test_secnums(app, status, warning):
assert doc2 == expect
app.config.text_add_secnumbers = False
- app.builder.build_all()
+ app.build(force_all=True)
index = (app.outdir / 'index.txt').read_text(encoding='utf8')
lines = index.splitlines()
assert lines[0] == "* Section A"
diff --git a/tests/test_builders/test_build_warnings.py b/tests/test_builders/test_build_warnings.py
new file mode 100644
index 0000000..eeb7e9d
--- /dev/null
+++ b/tests/test_builders/test_build_warnings.py
@@ -0,0 +1,89 @@
+import os
+import re
+import sys
+
+import pytest
+
+from sphinx.util.console import strip_colors
+
+ENV_WARNINGS = """\
+{root}/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
+WARNING: Explicit markup ends without a blank line; unexpected unindent.
+{root}/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
+file '{root}/wrongenc.inc' seems to be wrong, try giving an :encoding: option
+{root}/index.rst:\\d+: WARNING: invalid single index entry ''
+{root}/index.rst:\\d+: WARNING: image file not readable: foo.png
+{root}/index.rst:\\d+: WARNING: download file not readable: {root}/nonexisting.png
+{root}/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
+with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
+"""
+
+HTML_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
+"""
+
+LATEX_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
+"""
+
+TEXINFO_WARNINGS = ENV_WARNINGS + """\
+{root}/index.rst:\\d+: WARNING: unknown option: '&option'
+{root}/index.rst:\\d+: WARNING: citation not found: missing
+{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\*
+{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \
+\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\)
+"""
+
+
+def _check_warnings(expected_warnings: str, warning: str) -> None:
+ warnings = strip_colors(re.sub(re.escape(os.sep) + '{1,2}', '/', warning))
+ assert re.match(f'{expected_warnings}$', warnings), (
+ "Warnings don't match:\n"
+ + f'--- Expected (regex):\n{expected_warnings}\n'
+ + f'--- Got:\n{warnings}'
+ )
+ sys.modules.pop('autodoc_fodder', None)
+
+
+@pytest.mark.sphinx('html', testroot='warnings', freshenv=True)
+def test_html_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = HTML_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True)
+def test_latex_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = LATEX_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True)
+def test_texinfo_warnings(app, warning):
+ app.build(force_all=True)
+ warnings_exp = TEXINFO_WARNINGS.format(root=re.escape(app.srcdir.as_posix()))
+ _check_warnings(warnings_exp, warning.getvalue())
+
+
+def test_uncacheable_config_warning(make_app, tmp_path):
+ """Test that an unpickleable config value raises a warning."""
+ tmp_path.joinpath('conf.py').write_text("""\
+my_config = lambda: None
+show_warning_types = True
+def setup(app):
+ app.add_config_value('my_config', None, 'env')
+ """, encoding='utf-8')
+ tmp_path.joinpath('index.rst').write_text('Test\n====\n', encoding='utf-8')
+ app = make_app(srcdir=tmp_path)
+ app.build()
+ assert strip_colors(app.warning.getvalue()).strip() == (
+ "WARNING: cannot cache unpickable configuration value: 'my_config' "
+ "(because it contains a function, class, or module object) [config.cache]"
+ )
diff --git a/tests/test_builder.py b/tests/test_builders/test_builder.py
index 1ff8aea..ee946a5 100644
--- a/tests/test_builder.py
+++ b/tests/test_builders/test_builder.py
@@ -1,4 +1,7 @@
"""Test the Builder class."""
+
+import sys
+
import pytest
@@ -37,3 +40,5 @@ def test_incremental_reading_for_missing_files(app):
# "index" is listed up to updated because it contains references
# to nonexisting downloadable or image files
assert set(updated) == {'index'}
+
+ sys.modules.pop('autodoc_fodder', None)
diff --git a/tests/test_builders/xpath_data.py b/tests/test_builders/xpath_data.py
new file mode 100644
index 0000000..30f8e07
--- /dev/null
+++ b/tests/test_builders/xpath_data.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Final
+
+FIGURE_CAPTION: Final[str] = ".//figure/figcaption/p"
diff --git a/tests/test_builders/xpath_util.py b/tests/test_builders/xpath_util.py
new file mode 100644
index 0000000..7525c19
--- /dev/null
+++ b/tests/test_builders/xpath_util.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import re
+import textwrap
+from typing import TYPE_CHECKING
+from xml.etree.ElementTree import tostring
+
+if TYPE_CHECKING:
+ import os
+ from collections.abc import Callable, Iterable, Sequence
+ from xml.etree.ElementTree import Element, ElementTree
+
+
+def _get_text(node: Element) -> str:
+ if node.text is not None:
+ # the node has only one text
+ return node.text
+
+ # the node has tags and text; gather texts just under the node
+ return ''.join(n.tail or '' for n in node)
+
+
+def _prettify(nodes: Iterable[Element]) -> str:
+ def pformat(node: Element) -> str:
+ return tostring(node, encoding='unicode', method='html')
+
+ return ''.join(f'(i={index}) {pformat(node)}\n' for index, node in enumerate(nodes))
+
+
+def check_xpath(
+ etree: ElementTree,
+ filename: str | os.PathLike[str],
+ xpath: str,
+ check: str | re.Pattern[str] | Callable[[Sequence[Element]], None] | None,
+ be_found: bool = True,
+ *,
+ min_count: int = 1,
+) -> None:
+ """Check that one or more nodes satisfy a predicate.
+
+ :param etree: The element tree.
+ :param filename: The element tree source name (for errors only).
+ :param xpath: An XPath expression to use.
+ :param check: Optional regular expression or a predicate the nodes must validate.
+ :param be_found: If false, negate the predicate.
+ :param min_count: Minimum number of nodes expected to satisfy the predicate.
+
+ * If *check* is empty (``''``), only the minimum count is checked.
+ * If *check* is ``None``, no node should satisfy the XPath expression.
+ """
+ nodes = etree.findall(xpath)
+ assert isinstance(nodes, list)
+
+ if check is None:
+ # use == to have a nice pytest diff
+ assert nodes == [], f'found nodes matching xpath {xpath!r} in file {filename}'
+ return
+
+ assert len(nodes) >= min_count, (f'expecting at least {min_count} node(s) '
+ f'to satisfy {xpath!r} in file {filename}')
+
+ if check == '':
+ return
+
+ if callable(check):
+ check(nodes)
+ return
+
+ rex = re.compile(check)
+ if be_found:
+ if any(rex.search(_get_text(node)) for node in nodes):
+ return
+ else:
+ if all(not rex.search(_get_text(node)) for node in nodes):
+ return
+
+ ctx = textwrap.indent(_prettify(nodes), ' ' * 2)
+ msg = f'{check!r} not found in any node matching {xpath!r} in file {filename}:\n{ctx}'
+ raise AssertionError(msg)