diff options
author | Adam Turner <9087854+aa-turner@users.noreply.github.com> | 2022-09-10 17:26:41 +0100 |
---|---|---|
committer | Adam Turner <9087854+aa-turner@users.noreply.github.com> | 2023-03-24 00:29:27 +0000 |
commit | 97f07ca83c70bba55fe1c1cb25993e5240e4187d (patch) | |
tree | dcdf7833f4b3222340cb8182a33e547d2709ab42 | |
parent | 9df3b59e004837e5cabfdab0948bd579a6f691a4 (diff) | |
download | sphinx-git-97f07ca83c70bba55fe1c1cb25993e5240e4187d.tar.gz |
Speed up ``test_linkcheck``
18 files changed, 192 insertions, 120 deletions
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 3b283d98f..e828fc209 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -571,7 +571,7 @@ def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value('linkcheck_auth', [], False) app.add_config_value('linkcheck_request_headers', {}, False) app.add_config_value('linkcheck_retries', 1, False) - app.add_config_value('linkcheck_timeout', None, False, [int]) + app.add_config_value('linkcheck_timeout', None, False, [int, float]) app.add_config_value('linkcheck_workers', 5, False) app.add_config_value('linkcheck_anchors', True, False) # Anchors starting with ! are ignored since they are diff --git a/tests/roots/test-linkcheck-anchors-ignore/conf.py b/tests/roots/test-linkcheck-anchors-ignore/conf.py new file mode 100644 index 000000000..798f0bdda --- /dev/null +++ b/tests/roots/test-linkcheck-anchors-ignore/conf.py @@ -0,0 +1,3 @@ +exclude_patterns = ['_build'] +linkcheck_anchors = True +linkcheck_timeout = 0.02 diff --git a/tests/roots/test-linkcheck-anchors-ignore/index.rst b/tests/roots/test-linkcheck-anchors-ignore/index.rst new file mode 100644 index 000000000..22a137911 --- /dev/null +++ b/tests/roots/test-linkcheck-anchors-ignore/index.rst @@ -0,0 +1,2 @@ +* `Example Bar invalid <http://localhost:7777/#!bar>`_ +* `Example Bar invalid <http://localhost:7777/#top>`_ diff --git a/tests/roots/test-linkcheck-documents_exclude/conf.py b/tests/roots/test-linkcheck-documents_exclude/conf.py index 65ecbfb36..a6490f98b 100644 --- a/tests/roots/test-linkcheck-documents_exclude/conf.py +++ b/tests/roots/test-linkcheck-documents_exclude/conf.py @@ -3,3 +3,4 @@ linkcheck_exclude_documents = [ '^broken_link$', 'br[0-9]ken_link', ] +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck-localserver-anchor/conf.py b/tests/roots/test-linkcheck-localserver-anchor/conf.py index 2ba1f85e8..c00d59b08 100644 --- a/tests/roots/test-linkcheck-localserver-anchor/conf.py +++ b/tests/roots/test-linkcheck-localserver-anchor/conf.py @@ -1,2 +1,3 @@ exclude_patterns = ['_build'] linkcheck_anchors = True +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck-localserver-https/conf.py b/tests/roots/test-linkcheck-localserver-https/conf.py index a45d22e28..0fa7eb7f2 100644 --- a/tests/roots/test-linkcheck-localserver-https/conf.py +++ b/tests/roots/test-linkcheck-localserver-https/conf.py @@ -1 +1,6 @@ +import sys + exclude_patterns = ['_build'] +linkcheck_timeout = 0.01 if sys.platform != 'win32' else 0.05 + +del sys diff --git a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py index a45d22e28..7733c5910 100644 --- a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py +++ b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py @@ -1 +1,2 @@ exclude_patterns = ['_build'] +linkcheck_timeout = 0.02 diff --git a/tests/roots/test-linkcheck-localserver-warn-redirects/index.rst b/tests/roots/test-linkcheck-localserver-warn-redirects/index.rst index 7c57d5671..7359bd584 100644 --- a/tests/roots/test-linkcheck-localserver-warn-redirects/index.rst +++ b/tests/roots/test-linkcheck-localserver-warn-redirects/index.rst @@ -1,2 +1,3 @@ `local server1 <http://localhost:7777/path1>`_ + `local server2 <http://localhost:7777/path2>`_ diff --git a/tests/roots/test-linkcheck-localserver/conf.py b/tests/roots/test-linkcheck-localserver/conf.py index a45d22e28..f16f11c82 100644 --- a/tests/roots/test-linkcheck-localserver/conf.py +++ b/tests/roots/test-linkcheck-localserver/conf.py @@ -1 +1,2 @@ exclude_patterns = ['_build'] +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck-raw-node/conf.py b/tests/roots/test-linkcheck-raw-node/conf.py new file mode 100644 index 000000000..f16f11c82 --- /dev/null +++ b/tests/roots/test-linkcheck-raw-node/conf.py @@ -0,0 +1,2 @@ +exclude_patterns = ['_build'] +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck-raw-node/index.rst b/tests/roots/test-linkcheck-raw-node/index.rst new file mode 100644 index 000000000..76e26b547 --- /dev/null +++ b/tests/roots/test-linkcheck-raw-node/index.rst @@ -0,0 +1,2 @@ +.. raw:: html + :url: http://localhost:7777/ diff --git a/tests/roots/test-linkcheck-too-many-retries/conf.py b/tests/roots/test-linkcheck-too-many-retries/conf.py new file mode 100644 index 000000000..c00d59b08 --- /dev/null +++ b/tests/roots/test-linkcheck-too-many-retries/conf.py @@ -0,0 +1,3 @@ +exclude_patterns = ['_build'] +linkcheck_anchors = True +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck-too-many-retries/index.rst b/tests/roots/test-linkcheck-too-many-retries/index.rst new file mode 100644 index 000000000..29b1ae450 --- /dev/null +++ b/tests/roots/test-linkcheck-too-many-retries/index.rst @@ -0,0 +1 @@ +`Non-existing uri with localhost <https://localhost:7777/doesnotexist>`_ diff --git a/tests/roots/test-linkcheck/conf.py b/tests/roots/test-linkcheck/conf.py index ac54f73b0..078329f6c 100644 --- a/tests/roots/test-linkcheck/conf.py +++ b/tests/roots/test-linkcheck/conf.py @@ -1,4 +1,4 @@ root_doc = 'links' -source_suffix = '.txt' exclude_patterns = ['_build'] linkcheck_anchors = True +linkcheck_timeout = 0.01 diff --git a/tests/roots/test-linkcheck/links.rst b/tests/roots/test-linkcheck/links.rst new file mode 100644 index 000000000..49afba2b3 --- /dev/null +++ b/tests/roots/test-linkcheck/links.rst @@ -0,0 +1,13 @@ +Some additional anchors to exercise ignore code + +* `Valid url <http://localhost:7777/>`_ +* `Bar anchor invalid (trailing slash) <http://localhost:7777/#!bar>`_ +* `Bar anchor invalid <http://localhost:7777#!bar>`_ tests that default ignore anchor of #! does not need to be prefixed with / +* `Top anchor invalid <http://localhost:7777/#top>`_ +* `'does-not-exist' anchor invalid <http://localhost:7777#does-not-exist>`_ +* `Valid local file <conf.py>`_ +* `Invalid local file <path/to/notfound>`_ + +.. image:: http://localhost:7777/image.png +.. figure:: http://localhost:7777/image2.png + diff --git a/tests/roots/test-linkcheck/links.txt b/tests/roots/test-linkcheck/links.txt deleted file mode 100644 index 1d2408401..000000000 --- a/tests/roots/test-linkcheck/links.txt +++ /dev/null @@ -1,22 +0,0 @@ -This is from CPython documentation. - -* Also, if there is a `default namespace <https://www.w3.org/TR/2006/REC-xml-names-20060816/#defaulting>`__, that full URI gets prepended to all of the non-prefixed tags. - -* The URL having anchor: `https://www.sphinx-doc.org/en/master/usage/installation.html#overview`_ - -Some additional anchors to exercise ignore code - -* `Example Bar invalid <https://www.google.com/#!bar>`_ -* `Example Bar invalid <https://www.google.com#!bar>`_ tests that default ignore anchor of #! does not need to be prefixed with / -* `Example Bar invalid <https://www.google.com/#top>`_ -* `Example anchor invalid <http://www.sphinx-doc.org/en/master/index.html#does-not-exist>`_ -* `Complete nonsense <https://localhost:7777/doesnotexist>`_ -* `Example valid local file <conf.py>`_ -* `Example invalid local file <path/to/notfound>`_ -* https://github.com/sphinx-doc/sphinx/blob/master/sphinx/__init__.py#L2 - -.. image:: https://www.google.com/image.png -.. figure:: https://www.google.com/image2.png - -.. raw:: html - :url: https://www.sphinx-doc.org/ diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 57a4075b9..0592eb3ae 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -10,6 +10,7 @@ import textwrap import time import wsgiref.handlers from datetime import datetime +from os import path from queue import Queue from unittest import mock @@ -22,101 +23,137 @@ from sphinx.util.console import strip_colors from .utils import CERT_FILE, http_server, https_server 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): + def do_HEAD(self): + if self.path[1:].rstrip() == "": + self.send_response(200, "OK") + self.end_headers() + else: + self.send_response(404, "Not Found") + self.end_headers() + + def do_GET(self): + self.do_HEAD() + if self.path[1:].rstrip() == "": + self.wfile.write(b"ok\n\n") @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) def test_defaults(app): - app.build() + with http_server(DefaultsHandler): + app.build() + # Text output assert (app.outdir / 'output.txt').exists() content = (app.outdir / 'output.txt').read_text(encoding='utf8') - print(content) # looking for '#top' and '#does-not-exist' not found should fail assert "Anchor 'top' not found" in content assert "Anchor 'does-not-exist' not found" in content - # looking for non-existent URL should fail - assert " Max retries exceeded with url: /doesnotexist" in content # images should fail - assert "Not Found for url: https://www.google.com/image.png" in content - assert "Not Found for url: https://www.google.com/image2.png" in content + 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 "[broken] path/to/notfound" in content - assert len(content.splitlines()) == 7 - - -@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) -def test_defaults_json(app): - app.build() + assert len(content.splitlines()) == 5 + # JSON output assert (app.outdir / 'output.json').exists() content = (app.outdir / 'output.json').read_text(encoding='utf8') - print(content) rows = [json.loads(x) for x in content.splitlines()] row = rows[0] - for attr in ["filename", "lineno", "status", "code", "uri", - "info"]: + for attr in ("filename", "lineno", "status", "code", "uri", "info"): assert attr in row - assert len(content.splitlines()) == 12 - assert len(rows) == 12 + assert len(content.splitlines()) == 9 + assert len(rows) == 9 # 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["https://www.google.com#!bar"] == { - 'filename': 'links.txt', - 'lineno': 10, + assert rowsby["http://localhost:7777#!bar"] == { + 'filename': 'links.rst', + 'lineno': 5, 'status': 'working', 'code': 0, - 'uri': 'https://www.google.com#!bar', + 'uri': 'http://localhost:7777#!bar', 'info': '', } - # looking for non-existent URL should fail - dnerow = rowsby['https://localhost:7777/doesnotexist'] - assert dnerow['filename'] == 'links.txt' - assert dnerow['lineno'] == 13 - assert dnerow['status'] == 'broken' - assert dnerow['code'] == 0 - assert dnerow['uri'] == 'https://localhost:7777/doesnotexist' - assert rowsby['https://www.google.com/image2.png'] == { - 'filename': 'links.txt', - 'lineno': 20, + assert rowsby['http://localhost:7777/image2.png'] == { + 'filename': 'links.rst', + 'lineno': 13, 'status': 'broken', 'code': 0, - 'uri': 'https://www.google.com/image2.png', - 'info': '404 Client Error: Not Found for url: https://www.google.com/image2.png', + 'uri': 'http://localhost:7777/image2.png', + 'info': '404 Client Error: Not Found for url: http://localhost:7777/image2.png', } # looking for '#top' and '#does-not-exist' not found should fail - assert rowsby["https://www.google.com/#top"]["info"] == "Anchor 'top' not found" - assert rowsby["http://www.sphinx-doc.org/en/master/index.html#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found" + assert rowsby["http://localhost:7777/#top"]["info"] == "Anchor 'top' not found" + assert rowsby["http://localhost:7777#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found" # images should fail - assert "Not Found for url: https://www.google.com/image.png" in \ - rowsby["https://www.google.com/image.png"]["info"] + assert "Not Found for url: http://localhost:7777/image.png" in rowsby["http://localhost:7777/image.png"]["info"] + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True) +def test_too_many_retries(app): + app.build() + + # Text output + assert (app.outdir / 'output.txt').exists() + content = (app.outdir / 'output.txt').read_text(encoding='utf8') + + # looking for non-existent URL should fail + assert " Max retries exceeded with url: /doesnotexist" in content + + # JSON output + assert (app.outdir / 'output.json').exists() + content = (app.outdir / 'output.json').read_text(encoding='utf8') + + assert len(content.splitlines()) == 1 + row = json.loads(content) + # the output order of the rows is not stable + # due to possible variance in network latency + + # looking for non-existent URL should fail + assert row['filename'] == 'index.rst' + assert row['lineno'] == 1 + assert row['status'] == 'broken' + assert row['code'] == 0 + assert row['uri'] == 'https://localhost:7777/doesnotexist' + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True) +def test_raw_node(app): + with http_server(OKHandler): + app.build() + + # JSON output + assert (app.outdir / 'output.json').exists() + content = (app.outdir / 'output.json').read_text(encoding='utf8') + + assert len(content.splitlines()) == 1 + row = json.loads(content) + # raw nodes' url should be checked too - assert rowsby["https://www.sphinx-doc.org/"] == { - 'filename': 'links.txt', - 'lineno': 21, - 'status': 'redirected', - 'code': 302, - 'uri': 'https://www.sphinx-doc.org/', - 'info': 'https://www.sphinx-doc.org/en/master/', + assert row == { + 'filename': 'index.rst', + 'lineno': 1, + 'status': 'working', + 'code': 0, + 'uri': 'http://localhost:7777/', + 'info': '', } @pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck', freshenv=True, - confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"], - 'linkcheck_ignore': [ - 'https://localhost:7777/doesnotexist', - 'http://www.sphinx-doc.org/en/master/index.html#', - 'https://www.sphinx-doc.org/', - 'https://www.google.com/image.png', - 'https://www.google.com/image2.png', - 'path/to/notfound'], - }) + 'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True, + confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]}) def test_anchors_ignored(app): - app.build() + with http_server(OKHandler): + app.build() assert (app.outdir / 'output.txt').exists() content = (app.outdir / 'output.txt').read_text(encoding='utf8') @@ -141,14 +178,16 @@ def test_raises_for_invalid_status(app): ) -class HeadersDumperHandler(http.server.BaseHTTPRequestHandler): - def do_HEAD(self): - self.do_GET() +def capture_headers_handler(records): + class HeadersDumperHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + self.do_GET() - def do_GET(self): - self.send_response(200, "OK") - self.end_headers() - print(self.headers.as_string()) + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + records.append(self.headers.as_string()) + return HeadersDumperHandler @pytest.mark.sphinx( @@ -158,10 +197,12 @@ class HeadersDumperHandler(http.server.BaseHTTPRequestHandler): (r'^http://localhost:7777/$', ('user1', 'password')), (r'.*local.*', ('user2', 'hunter2')), ]}) -def test_auth_header_uses_first_match(app, capsys): - with http_server(HeadersDumperHandler): +def test_auth_header_uses_first_match(app): + records = [] + with http_server(capture_headers_handler(records)): app.build() - stdout, stderr = capsys.readouterr() + + stdout = "\n".join(records) encoded_auth = base64.b64encode(b'user1:password').decode('ascii') assert f"Authorization: Basic {encoded_auth}\n" in stdout @@ -169,10 +210,12 @@ def test_auth_header_uses_first_match(app, capsys): @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]}) -def test_auth_header_no_match(app, capsys): - with http_server(HeadersDumperHandler): +def test_auth_header_no_match(app): + records = [] + with http_server(capture_headers_handler(records)): app.build() - stdout, stderr = capsys.readouterr() + + stdout = "\n".join(records) assert "Authorization" not in stdout @@ -186,11 +229,12 @@ def test_auth_header_no_match(app, capsys): "X-Secret": "open sesami", }, }}) -def test_linkcheck_request_headers(app, capsys): - with http_server(HeadersDumperHandler): +def test_linkcheck_request_headers(app): + records = [] + with http_server(capture_headers_handler(records)): app.build() - stdout, _stderr = capsys.readouterr() + stdout = "\n".join(records) assert "Accept: text/html\n" in stdout assert "X-Secret" not in stdout assert "sesami" not in stdout @@ -202,11 +246,12 @@ def test_linkcheck_request_headers(app, capsys): "http://localhost:7777": {"Accept": "application/json"}, "*": {"X-Secret": "open sesami"}, }}) -def test_linkcheck_request_headers_no_slash(app, capsys): - with http_server(HeadersDumperHandler): +def test_linkcheck_request_headers_no_slash(app): + records = [] + with http_server(capture_headers_handler(records)): app.build() - stdout, _stderr = capsys.readouterr() + stdout = "\n".join(records) assert "Accept: application/json\n" in stdout assert "X-Secret" not in stdout assert "sesami" not in stdout @@ -218,11 +263,12 @@ def test_linkcheck_request_headers_no_slash(app, capsys): "http://do.not.match.org": {"Accept": "application/json"}, "*": {"X-Secret": "open sesami"}, }}) -def test_linkcheck_request_headers_default(app, capsys): - with http_server(HeadersDumperHandler): +def test_linkcheck_request_headers_default(app): + records = [] + with http_server(capture_headers_handler(records)): app.build() - stdout, _stderr = capsys.readouterr() + stdout = "\n".join(records) assert "Accepts: application/json\n" not in stdout assert "X-Secret: open sesami\n" in stdout @@ -299,14 +345,21 @@ def test_linkcheck_allowed_redirects(app, warning): app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: - records = [json.loads(l) for l in fp.readlines()] - - assert len(records) == 2 - result = {r["uri"]: r["status"] for r in records} - assert result["http://localhost:7777/path1"] == "working" - assert result["http://localhost:7777/path2"] == "redirected" + rows = [json.loads(l) for l in fp.readlines()] + + 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"] == { + 'filename': 'index.rst', + 'lineno': 3, + 'status': 'redirected', + 'code': 302, + 'uri': 'http://localhost:7777/path2', + 'info': 'http://localhost:7777/?redirected=1', + } - assert ("index.rst:1: WARNING: redirect http://localhost:7777/path2 - with Found to " + 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 len(warning.getvalue().splitlines()) == 1 @@ -422,18 +475,23 @@ def test_connect_to_selfsigned_nonexistent_cert_file(app): } +class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_response(302, "Found") + self.send_header("Location", "http://localhost:7777/") + self.end_headers() + + def do_GET(self): + self.send_response(200, "OK") + self.end_headers() + self.wfile.write(b"ok\n") + + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_TooManyRedirects_on_HEAD(app): - class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler): - def do_HEAD(self): - self.send_response(302, "Found") - self.send_header("Location", "http://localhost:7777/") - self.end_headers() +def test_TooManyRedirects_on_HEAD(app, monkeypatch): + import requests.sessions - def do_GET(self): - self.send_response(200, "OK") - self.end_headers() - self.wfile.write(b"ok\n") + monkeypatch.setattr(requests.sessions, "DEFAULT_REDIRECT_LIMIT", 5) with http_server(InfiniteRedirectOnHeadHandler): app.build() @@ -540,7 +598,7 @@ def test_too_many_requests_retry_after_without_header(app, capsys): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_too_many_requests_user_timeout(app, capsys): +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)])): app.build() diff --git a/tests/utils.py b/tests/utils.py index 1f7cbb05a..a0c00a6dd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,7 +16,7 @@ class HttpServerThread(threading.Thread): self.server = http.server.HTTPServer(("localhost", 7777), handler) def run(self): - self.server.serve_forever(poll_interval=0.01) + self.server.serve_forever(poll_interval=0.001) def terminate(self): self.server.shutdown() |