77import sys
88from collections import namedtuple
99from io import StringIO
10- from typing import TYPE_CHECKING , Any , Callable
10+ from typing import TYPE_CHECKING , Optional
1111
1212import pytest
1313
1414from sphinx .testing .util import SphinxTestApp , SphinxTestAppWrapperForSkipBuilding
1515
1616if TYPE_CHECKING :
17- from collections .abc import Generator
17+ from collections .abc import Callable , Generator
1818 from pathlib import Path
19+ from typing import Any
1920
2021DEFAULT_ENABLED_MARKERS = [
2122 (
@@ -60,8 +61,13 @@ def restore(self, key: str) -> dict[str, StringIO]:
6061
6162
6263@pytest .fixture ()
63- def app_params (request : Any , test_params : dict , shared_result : SharedResult ,
64- sphinx_test_tempdir : str , rootdir : str ) -> _app_params :
64+ def app_params (
65+ request : Any ,
66+ test_params : dict ,
67+ shared_result : SharedResult ,
68+ sphinx_test_tempdir : str ,
69+ rootdir : str ,
70+ ) -> _app_params :
6571 """
6672 Parameters that are specified by 'pytest.mark.sphinx' for
6773 sphinx.application.Sphinx initialization
@@ -128,8 +134,12 @@ def test_params(request: Any) -> dict:
128134
129135
130136@pytest .fixture ()
131- def app (test_params : dict , app_params : tuple [dict , dict ], make_app : Callable ,
132- shared_result : SharedResult ) -> Generator [SphinxTestApp , None , None ]:
137+ def app (
138+ test_params : dict ,
139+ app_params : tuple [dict , dict ],
140+ make_app : Callable ,
141+ shared_result : SharedResult ,
142+ ) -> Generator [SphinxTestApp , None , None ]:
133143 """
134144 Provides the 'sphinx.application.Sphinx' object
135145 """
@@ -218,6 +228,52 @@ def if_graphviz_found(app: SphinxTestApp) -> None: # NoQA: PT004
218228 pytest .skip ('graphviz "dot" is not available' )
219229
220230
231+ _HOST_ONLINE_ERROR = pytest .StashKey [Optional [str ]]()
232+
233+
234+ def _query (address : tuple [str , int ]) -> str | None :
235+ import socket
236+
237+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as sock :
238+ try :
239+ sock .settimeout (5 )
240+ sock .connect (address )
241+ except OSError as exc :
242+ # other type of errors are propagated
243+ return str (exc )
244+ return None
245+
246+
247+ @pytest .fixture (scope = 'session' )
248+ def sphinx_remote_query_address () -> tuple [str , int ]:
249+ """Address to which a query is made to check that the host is online.
250+
251+ By default, onlineness is tested by querying the DNS server ``1.1.1.1``
252+ but users concerned about privacy might change it in ``conftest.py``.
253+ """
254+ return ('1.1.1.1' , 80 )
255+
256+
257+ @pytest .fixture (scope = 'session' )
258+ def if_online ( # NoQA: PT004
259+ request : pytest .FixtureRequest ,
260+ sphinx_remote_query_address : tuple [str , int ],
261+ ) -> None :
262+ """Skip the test if the host has no connection.
263+
264+ Usage::
265+
266+ @pytest.mark.usefixtures('if_online')
267+ def test_if_host_is_online(): ...
268+ """
269+ if _HOST_ONLINE_ERROR not in request .session .stash :
270+ # do not use setdefault() to avoid creating a socket connection
271+ lookup_error = _query (sphinx_remote_query_address )
272+ request .session .stash [_HOST_ONLINE_ERROR ] = lookup_error
273+ if (error := request .session .stash [_HOST_ONLINE_ERROR ]) is not None :
274+ pytest .skip ('host appears to be offline (%s)' % error )
275+
276+
221277@pytest .fixture (scope = 'session' )
222278def sphinx_test_tempdir (tmp_path_factory : Any ) -> Path :
223279 """Temporary directory."""
@@ -233,8 +289,8 @@ def rollback_sysmodules() -> Generator[None, None, None]: # NoQA: PT004
233289 For example, used in test_ext_autosummary.py to permit unloading the
234290 target module to clear its cache.
235291 """
292+ sysmodules = list (sys .modules )
236293 try :
237- sysmodules = list (sys .modules )
238294 yield
239295 finally :
240296 for modname in list (sys .modules ):
0 commit comments