linkcheck: add a distinct 'timeout' reporting status (#11876)

This commit is contained in:
James Addison 2024-01-13 08:14:06 +00:00 committed by GitHub
parent 85400a0430
commit 9e198c70fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 38 additions and 1 deletions

View File

@ -57,6 +57,8 @@ Bugs fixed
Set this option to ``False`` to report HTTP 401 (unauthorized) server Set this option to ``False`` to report HTTP 401 (unauthorized) server
responses as broken. responses as broken.
Patch by James Addison. Patch by James Addison.
* #11868: linkcheck: added a distinct ``timeout`` reporting status code.
Patch by James Addison.
Testing Testing
------- -------

View File

@ -17,6 +17,7 @@ from urllib.parse import unquote, urlparse, urlsplit, urlunparse
from docutils import nodes from docutils import nodes
from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
from requests.exceptions import Timeout as RequestTimeout
from sphinx.builders.dummy import DummyBuilder from sphinx.builders.dummy import DummyBuilder
from sphinx.deprecation import RemovedInSphinx80Warning from sphinx.deprecation import RemovedInSphinx80Warning
@ -64,6 +65,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
def init(self) -> None: def init(self) -> None:
self.broken_hyperlinks = 0 self.broken_hyperlinks = 0
self.timed_out_hyperlinks = 0
self.hyperlinks: dict[str, Hyperlink] = {} self.hyperlinks: dict[str, Hyperlink] = {}
# set a timeout for non-responding servers # set a timeout for non-responding servers
socket.setdefaulttimeout(5.0) socket.setdefaulttimeout(5.0)
@ -88,7 +90,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
for result in checker.check(self.hyperlinks): for result in checker.check(self.hyperlinks):
self.process_result(result) self.process_result(result)
if self.broken_hyperlinks: if self.broken_hyperlinks or self.timed_out_hyperlinks:
self.app.statuscode = 1 self.app.statuscode = 1
def process_result(self, result: CheckResult) -> None: def process_result(self, result: CheckResult) -> None:
@ -115,6 +117,15 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.write_entry('local', result.docname, filename, result.lineno, result.uri) self.write_entry('local', result.docname, filename, result.lineno, result.uri)
elif result.status == 'working': elif result.status == 'working':
logger.info(darkgreen('ok ') + result.uri + result.message) logger.info(darkgreen('ok ') + result.uri + result.message)
elif result.status == 'timeout':
if self.app.quiet or self.app.warningiserror:
logger.warning('timeout ' + result.uri + result.message,
location=(result.docname, result.lineno))
else:
logger.info(red('timeout ') + result.uri + red(' - ' + result.message))
self.write_entry('timeout', result.docname, filename, result.lineno,
result.uri + ': ' + result.message)
self.timed_out_hyperlinks += 1
elif result.status == 'broken': elif result.status == 'broken':
if self.app.quiet or self.app.warningiserror: if self.app.quiet or self.app.warningiserror:
logger.warning(__('broken link: %s (%s)'), result.uri, result.message, logger.warning(__('broken link: %s (%s)'), result.uri, result.message,
@ -436,6 +447,9 @@ class HyperlinkAvailabilityCheckWorker(Thread):
del response del response
break break
except RequestTimeout as err:
return 'timeout', str(err), 0
except SSLError as err: except SSLError as err:
# SSL failure; report that the link is broken. # SSL failure; report that the link is broken.
return 'broken', str(err), 0 return 'broken', str(err), 0

View File

@ -854,6 +854,27 @@ 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):
class DelayedResponseHandler(http.server.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()
app.config.linkcheck_timeout = 0.01
with http_server(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) @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_too_many_requests_user_timeout(app): def test_too_many_requests_user_timeout(app):
app.config.linkcheck_rate_limit_timeout = 0.0 app.config.linkcheck_rate_limit_timeout = 0.0