Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typehints] tests: enable mypy for linkcheck builder tests. #12160

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
56ef23b
[typehints] tests: enable mypy for linkcheck builder tests.
jayaddison Mar 21, 2024
5fed4b3
[typehints] linkcheck tests: add typehinting to test cases.
jayaddison Mar 21, 2024
f538814
[typehints] tests: add return types in linkcheck builder tests.
jayaddison Mar 21, 2024
634cb4a
[typehints] tests: move a few test app settings back into sphinx pyte…
jayaddison Mar 21, 2024
be66cf6
[typehints] linkcheck: fixup: adjust parameter type to optional.
jayaddison Mar 21, 2024
f0a7a6b
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Mar 21, 2024
c6ab50b
[typehints] tests: enable mypy for linkcheck builder tests.
jayaddison Mar 21, 2024
0ffe2c4
[typehints] Remove commented-out module entry from mypy overrides.
jayaddison Mar 22, 2024
c2ed645
[refactor] tests: relocate linkcheck-related settings in test arrange…
jayaddison Mar 22, 2024
5110535
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Mar 22, 2024
2d049da
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Mar 24, 2024
f5e20c3
[tests] linkcheck: relocate authentication-check decorator function.
jayaddison Mar 25, 2024
5d4b224
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Mar 25, 2024
3fd3f5b
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Mar 28, 2024
3545153
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison Apr 5, 2024
e043f96
[typing] Fixup: restore typehints on test_linkcheck_allowed_redirects…
jayaddison Apr 5, 2024
79fb5ba
Merge branch 'master' into typehints/testutils-add-sphinx-typehints
jayaddison May 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ module = [
"tests.test_builders.test_build_gettext",
"tests.test_builders.test_build_html",
"tests.test_builders.test_build_latex",
"tests.test_builders.test_build_linkcheck",
# "tests.test_builders.test_build_linkcheck",
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
"tests.test_builders.test_build_texinfo",
# tests/test_config
"tests.test_config.test_config",
Expand Down
2 changes: 1 addition & 1 deletion sphinx/builders/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
else:
return 'redirected', response_url, 0

def limit_rate(self, response_url: str, retry_after: str) -> float | None:
def limit_rate(self, response_url: str, retry_after: str | None) -> float | None:
delay = DEFAULT_DELAY
next_check = None
if retry_after:
Expand Down
97 changes: 54 additions & 43 deletions tests/test_builders/test_build_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from base64 import b64encode
from http.server import BaseHTTPRequestHandler
from queue import Queue
from typing import TYPE_CHECKING
from unittest import mock

import docutils
Expand All @@ -20,6 +21,7 @@
import sphinx.util.http_date
from sphinx.builders.linkcheck import (
CheckRequest,
CheckResult,
Hyperlink,
HyperlinkAvailabilityCheckWorker,
RateLimit,
Expand All @@ -32,6 +34,11 @@

ts_re = re.compile(r".*\[(?P<ts>.*)\].*")

if TYPE_CHECKING:
from collections.abc import Callable

from sphinx.application import Sphinx


class DefaultsHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
Expand Down Expand Up @@ -100,7 +107,7 @@ def connection_count(self):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
def test_defaults(app):
def test_defaults(app: Sphinx) -> None:
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
with http_server(DefaultsHandler):
with ConnectionMeasurement() as m:
app.build()
Expand Down Expand Up @@ -145,7 +152,7 @@ def test_defaults(app):
'info': '',
}

def _missing_resource(filename: str, lineno: int):
def _missing_resource(filename: str, lineno: int) -> dict[str, str | int]:
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
return {
'filename': 'links.rst',
'lineno': lineno,
Expand Down Expand Up @@ -177,7 +184,7 @@ def _missing_resource(filename: str, lineno: int):
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck', freshenv=True,
confoverrides={'linkcheck_anchors': False})
def test_check_link_response_only(app):
def test_check_link_response_only(app: Sphinx) -> None:
with http_server(DefaultsHandler):
app.build()

Expand All @@ -191,7 +198,7 @@ def test_check_link_response_only(app):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True)
def test_too_many_retries(app):
def test_too_many_retries(app: Sphinx) -> None:
with http_server(DefaultsHandler):
app.build()

Expand Down Expand Up @@ -220,7 +227,7 @@ def test_too_many_retries(app):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True)
def test_raw_node(app):
def test_raw_node(app: Sphinx) -> None:
with http_server(OKHandler):
app.build()

Expand All @@ -245,7 +252,7 @@ def test_raw_node(app):
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True,
confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]})
def test_anchors_ignored(app):
def test_anchors_ignored(app: Sphinx) -> None:
with http_server(OKHandler):
app.build()

Expand Down Expand Up @@ -278,7 +285,7 @@ def do_GET(self):
'http://localhost:7777/ignored', # existing page
'http://localhost:7777/invalid', # unknown page
]})
def test_anchors_ignored_for_url(app):
def test_anchors_ignored_for_url(app: Sphinx) -> None:
with http_server(AnchorsIgnoreForUrlHandler):
app.build()

Expand Down Expand Up @@ -315,7 +322,7 @@ def test_anchors_ignored_for_url(app):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True)
def test_raises_for_invalid_status(app):
def test_raises_for_invalid_status(app: Sphinx) -> None:
class InternalServerErrorHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"

Expand Down Expand Up @@ -347,7 +354,9 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True):
class CustomHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"

def authenticated(method):
def authenticated( # type: ignore[misc]
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
method: Callable[[CustomHandler], None]
) -> Callable[[CustomHandler], None]:
def method_if_authenticated(self):
if expected_token is None:
return method(self)
Expand Down Expand Up @@ -387,7 +396,7 @@ def do_GET(self):
(r'^http://localhost:7777/$', ('user1', 'password')),
(r'.*local.*', ('user2', 'hunter2')),
]})
def test_auth_header_uses_first_match(app):
def test_auth_header_uses_first_match(app: Sphinx) -> None:
with http_server(custom_handler(valid_credentials=("user1", "password"))):
app.build()

Expand All @@ -401,7 +410,7 @@ def test_auth_header_uses_first_match(app):
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_allow_unauthorized': False})
def test_unauthorized_broken(app):
def test_unauthorized_broken(app: Sphinx) -> None:
with http_server(custom_handler(valid_credentials=("user1", "password"))):
app.build()

Expand All @@ -415,7 +424,7 @@ def test_unauthorized_broken(app):
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]})
def test_auth_header_no_match(app):
def test_auth_header_no_match(app: Sphinx) -> None:
with (
http_server(custom_handler(valid_credentials=("user1", "password"))),
pytest.warns(RemovedInSphinx80Warning, match='linkcheck builder encountered an HTTP 401'),
Expand All @@ -440,7 +449,7 @@ def test_auth_header_no_match(app):
"X-Secret": "open sesami",
},
}})
def test_linkcheck_request_headers(app):
def test_linkcheck_request_headers(app: Sphinx) -> None:
def check_headers(self):
if "X-Secret" in self.headers:
return False
Expand All @@ -463,7 +472,7 @@ def check_headers(self):
"http://localhost:7777": {"Accept": "application/json"},
"*": {"X-Secret": "open sesami"},
}})
def test_linkcheck_request_headers_no_slash(app):
def test_linkcheck_request_headers_no_slash(app: Sphinx) -> None:
def check_headers(self):
if "X-Secret" in self.headers:
return False
Expand All @@ -486,7 +495,7 @@ def check_headers(self):
"http://do.not.match.org": {"Accept": "application/json"},
"*": {"X-Secret": "open sesami"},
}})
def test_linkcheck_request_headers_default(app):
def test_linkcheck_request_headers_default(app: Sphinx) -> None:
def check_headers(self):
if self.headers["X-Secret"] != "open sesami":
return False
Expand Down Expand Up @@ -632,7 +641,7 @@ def test_invalid_ssl(get_request, app):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True)
def test_connect_to_selfsigned_fails(app):
def test_connect_to_selfsigned_fails(app: Sphinx) -> None:
with http_server(OKHandler, tls_enabled=True):
app.build()

Expand All @@ -645,9 +654,9 @@ def test_connect_to_selfsigned_fails(app):
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
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True,
confoverrides={'tls_verify': False})
def test_connect_to_selfsigned_with_tls_verify_false(app: Sphinx) -> None:
with http_server(OKHandler, tls_enabled=True):
app.build()

Expand All @@ -663,9 +672,9 @@ 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
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True,
confoverrides={'tls_cacerts': CERT_FILE})
def test_connect_to_selfsigned_with_tls_cacerts(app: Sphinx) -> None:
with http_server(OKHandler, tls_enabled=True):
app.build()

Expand Down Expand Up @@ -699,9 +708,9 @@ 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"
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True,
confoverrides={'tls_cacerts': "does/not/exist"})
def test_connect_to_selfsigned_nonexistent_cert_file(app: Sphinx) -> None:
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
with http_server(OKHandler, tls_enabled=True):
app.build()

Expand Down Expand Up @@ -858,8 +867,9 @@ def test_too_many_requests_retry_after_without_header(app, capsys):
)


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_requests_timeout(app):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_timeout': 0.01})
def test_requests_timeout(app: Sphinx) -> None:
class DelayedResponseHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"

Expand All @@ -869,7 +879,6 @@ def do_GET(self):
self.send_header("Content-Length", "0")
self.end_headers()

app.config.linkcheck_timeout = 0.01
with http_server(DelayedResponseHandler):
app.build()

Expand All @@ -879,9 +888,9 @@ def do_GET(self):
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
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True,
confoverrides={'linkcheck_rate_limit_timeout': 0.0})
def test_too_many_requests_user_timeout(app: Sphinx) -> None:
with http_server(make_retry_after_handler([(429, None)])):
app.build()
content = (app.outdir / 'output.json').read_text(encoding='utf8')
Expand All @@ -900,39 +909,39 @@ class FakeResponse:
url = "http://localhost/"


def test_limit_rate_default_sleep(app):
def test_limit_rate_default_sleep(app: Sphinx) -> None:
worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), {})
with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After"))
assert next_check == 60.0


def test_limit_rate_user_max_delay(app):
app.config.linkcheck_rate_limit_timeout = 0.0
def test_limit_rate_user_max_delay(app: Sphinx) -> None:
app.config.linkcheck_rate_limit_timeout = 0.0 # type: ignore[attr-defined]
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), {})
next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After"))
assert next_check is None


def test_limit_rate_doubles_previous_wait_time(app):
def test_limit_rate_doubles_previous_wait_time(app: Sphinx) -> None:
rate_limits = {"localhost": RateLimit(60.0, 0.0)}
worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits)
with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After"))
assert next_check == 120.0


def test_limit_rate_clips_wait_time_to_max_time(app):
app.config.linkcheck_rate_limit_timeout = 90.0
def test_limit_rate_clips_wait_time_to_max_time(app: Sphinx) -> None:
app.config.linkcheck_rate_limit_timeout = 90.0 # type: ignore[attr-defined]
rate_limits = {"localhost": RateLimit(60.0, 0.0)}
worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits)
with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After"))
assert next_check == 90.0


def test_limit_rate_bails_out_after_waiting_max_time(app):
app.config.linkcheck_rate_limit_timeout = 90.0
def test_limit_rate_bails_out_after_waiting_max_time(app: Sphinx) -> None:
app.config.linkcheck_rate_limit_timeout = 90.0 # type: ignore[attr-defined]
rate_limits = {"localhost": RateLimit(90.0, 0.0)}
worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits)
next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After"))
Expand All @@ -951,13 +960,15 @@ def test_connection_contention(get_adapter, app, capsys):

# Place a workload into the linkcheck queue
link_count = 10
rqueue, wqueue = Queue(), Queue()
wqueue: Queue[CheckRequest] = Queue()
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
rqueue: Queue[CheckResult] = 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)):
begin, checked = time.time(), []
begin = time.time()
checked: list[CheckResult] = []
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
threads = [
HyperlinkAvailabilityCheckWorker(
config=app.config,
Expand Down Expand Up @@ -993,7 +1004,7 @@ def do_GET(self):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_get_after_head_raises_connection_error(app):
def test_get_after_head_raises_connection_error(app: Sphinx) -> None:
with http_server(ConnectionResetHandler):
app.build()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
Expand All @@ -1010,7 +1021,7 @@ def test_get_after_head_raises_connection_error(app):


@pytest.mark.sphinx('linkcheck', testroot='linkcheck-documents_exclude', freshenv=True)
def test_linkcheck_exclude_documents(app):
def test_linkcheck_exclude_documents(app: Sphinx) -> None:
with http_server(DefaultsHandler):
app.build()

Expand Down