diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index eaf76e284..a62f34694 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -7,15 +7,16 @@ import subprocess import sys from collections import namedtuple from io import StringIO -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Optional import pytest from sphinx.testing.util import SphinxTestApp, SphinxTestAppWrapperForSkipBuilding if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Callable, Generator from pathlib import Path + from typing import Any DEFAULT_ENABLED_MARKERS = [ ( @@ -60,8 +61,13 @@ class SharedResult: @pytest.fixture() -def app_params(request: Any, test_params: dict, shared_result: SharedResult, - sphinx_test_tempdir: str, rootdir: str) -> _app_params: +def app_params( + request: Any, + test_params: dict, + shared_result: SharedResult, + sphinx_test_tempdir: str, + rootdir: str, +) -> _app_params: """ Parameters that are specified by 'pytest.mark.sphinx' for sphinx.application.Sphinx initialization @@ -128,8 +134,12 @@ def test_params(request: Any) -> dict: @pytest.fixture() -def app(test_params: dict, app_params: tuple[dict, dict], make_app: Callable, - shared_result: SharedResult) -> Generator[SphinxTestApp, None, None]: +def app( + test_params: dict, + app_params: tuple[dict, dict], + make_app: Callable, + shared_result: SharedResult, +) -> Generator[SphinxTestApp, None, None]: """ Provides the 'sphinx.application.Sphinx' object """ @@ -218,6 +228,52 @@ def if_graphviz_found(app: SphinxTestApp) -> None: # NoQA: PT004 pytest.skip('graphviz "dot" is not available') +_HOST_ONLINE_ERROR = pytest.StashKey[Optional[str]]() + + +def _query(address: tuple[str, int]) -> str | None: + import socket + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.settimeout(5) + sock.connect(address) + except OSError as exc: + # other type of errors are propagated + return str(exc) + return None + + +@pytest.fixture(scope='session') +def sphinx_remote_query_address() -> tuple[str, int]: + """Address to which a query is made to check that the host is online. + + By default, onlineness is tested by querying the DNS server ``1.1.1.1`` + but users concerned about privacy might change it in ``conftest.py``. + """ + return ('1.1.1.1', 80) + + +@pytest.fixture(scope='session') +def if_online( # NoQA: PT004 + request: pytest.FixtureRequest, + sphinx_remote_query_address: tuple[str, int], +) -> None: + """Skip the test if the host has no connection. + + Usage:: + + @pytest.mark.usefixtures('if_online') + def test_if_host_is_online(): ... + """ + if _HOST_ONLINE_ERROR not in request.session.stash: + # do not use setdefault() to avoid creating a socket connection + lookup_error = _query(sphinx_remote_query_address) + request.session.stash[_HOST_ONLINE_ERROR] = lookup_error + if (error := request.session.stash[_HOST_ONLINE_ERROR]) is not None: + pytest.skip('host appears to be offline (%s)' % error) + + @pytest.fixture(scope='session') def sphinx_test_tempdir(tmp_path_factory: Any) -> Path: """Temporary directory.""" @@ -233,8 +289,8 @@ def rollback_sysmodules() -> Generator[None, None, None]: # NoQA: PT004 For example, used in test_ext_autosummary.py to permit unloading the target module to clear its cache. """ + sysmodules = list(sys.modules) try: - sysmodules = list(sys.modules) yield finally: for modname in list(sys.modules): diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py index 8c3d309d4..66ba58df0 100644 --- a/tests/test_builders/test_build_html_image.py +++ b/tests/test_builders/test_build_html_image.py @@ -5,6 +5,7 @@ import docutils import pytest +@pytest.mark.usefixtures('if_online') @pytest.mark.sphinx('html', testroot='images') def test_html_remote_images(app, status, warning): app.build(force_all=True) @@ -24,6 +25,7 @@ def test_html_encoded_image(app, status, warning): assert (app.outdir / '_images/img_#1.png').exists() +@pytest.mark.usefixtures('if_online') @pytest.mark.sphinx('html', testroot='remote-logo') def test_html_remote_logo(app, status, warning): app.build(force_all=True) diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 2c2c3acdc..e9114b5c4 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -1398,6 +1398,7 @@ def test_latex_raw_directive(app, status, warning): assert 'LaTeX: abc def ghi' in result +@pytest.mark.usefixtures('if_online') @pytest.mark.sphinx('latex', testroot='images') def test_latex_images(app, status, warning): app.build(force_all=True)