244 lines
7.0 KiB
Python
244 lines
7.0 KiB
Python
|
"""Sphinx test fixtures for pytest"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import shutil
|
||
|
import subprocess
|
||
|
import sys
|
||
|
from collections import namedtuple
|
||
|
from io import StringIO
|
||
|
from typing import TYPE_CHECKING, Any, Callable
|
||
|
|
||
|
import pytest
|
||
|
|
||
|
from sphinx.testing.util import SphinxTestApp, SphinxTestAppWrapperForSkipBuilding
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from collections.abc import Generator
|
||
|
from pathlib import Path
|
||
|
|
||
|
DEFAULT_ENABLED_MARKERS = [
|
||
|
(
|
||
|
'sphinx(builder, testroot=None, freshenv=False, confoverrides=None, tags=None,'
|
||
|
' docutilsconf=None, parallel=0): arguments to initialize the sphinx test application.'
|
||
|
),
|
||
|
'test_params(shared_result=...): test parameters.',
|
||
|
]
|
||
|
|
||
|
|
||
|
def pytest_configure(config):
|
||
|
"""Register custom markers"""
|
||
|
for marker in DEFAULT_ENABLED_MARKERS:
|
||
|
config.addinivalue_line('markers', marker)
|
||
|
|
||
|
|
||
|
@pytest.fixture(scope='session')
|
||
|
def rootdir() -> str | None:
|
||
|
return None
|
||
|
|
||
|
|
||
|
class SharedResult:
|
||
|
cache: dict[str, dict[str, str]] = {}
|
||
|
|
||
|
def store(self, key: str, app_: SphinxTestApp) -> Any:
|
||
|
if key in self.cache:
|
||
|
return
|
||
|
data = {
|
||
|
'status': app_._status.getvalue(),
|
||
|
'warning': app_._warning.getvalue(),
|
||
|
}
|
||
|
self.cache[key] = data
|
||
|
|
||
|
def restore(self, key: str) -> dict[str, StringIO]:
|
||
|
if key not in self.cache:
|
||
|
return {}
|
||
|
data = self.cache[key]
|
||
|
return {
|
||
|
'status': StringIO(data['status']),
|
||
|
'warning': StringIO(data['warning']),
|
||
|
}
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
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
|
||
|
"""
|
||
|
|
||
|
# ##### process pytest.mark.sphinx
|
||
|
|
||
|
pargs = {}
|
||
|
kwargs: dict[str, Any] = {}
|
||
|
|
||
|
# to avoid stacking positional args
|
||
|
for info in reversed(list(request.node.iter_markers("sphinx"))):
|
||
|
for i, a in enumerate(info.args):
|
||
|
pargs[i] = a
|
||
|
kwargs.update(info.kwargs)
|
||
|
|
||
|
args = [pargs[i] for i in sorted(pargs.keys())]
|
||
|
|
||
|
# ##### process pytest.mark.test_params
|
||
|
if test_params['shared_result']:
|
||
|
if 'srcdir' in kwargs:
|
||
|
msg = 'You can not specify shared_result and srcdir in same time.'
|
||
|
raise pytest.Exception(msg)
|
||
|
kwargs['srcdir'] = test_params['shared_result']
|
||
|
restore = shared_result.restore(test_params['shared_result'])
|
||
|
kwargs.update(restore)
|
||
|
|
||
|
# ##### prepare Application params
|
||
|
|
||
|
testroot = kwargs.pop('testroot', 'root')
|
||
|
kwargs['srcdir'] = srcdir = sphinx_test_tempdir / kwargs.get('srcdir', testroot)
|
||
|
|
||
|
# special support for sphinx/tests
|
||
|
if rootdir and not srcdir.exists():
|
||
|
testroot_path = rootdir / ('test-' + testroot)
|
||
|
shutil.copytree(testroot_path, srcdir)
|
||
|
|
||
|
return _app_params(args, kwargs)
|
||
|
|
||
|
|
||
|
_app_params = namedtuple('_app_params', 'args,kwargs')
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def test_params(request: Any) -> dict:
|
||
|
"""
|
||
|
Test parameters that are specified by 'pytest.mark.test_params'
|
||
|
|
||
|
:param Union[str] shared_result:
|
||
|
If the value is provided, app._status and app._warning objects will be
|
||
|
shared in the parametrized test functions and/or test functions that
|
||
|
have same 'shared_result' value.
|
||
|
**NOTE**: You can not specify both shared_result and srcdir.
|
||
|
"""
|
||
|
env = request.node.get_closest_marker('test_params')
|
||
|
kwargs = env.kwargs if env else {}
|
||
|
result = {
|
||
|
'shared_result': None,
|
||
|
}
|
||
|
result.update(kwargs)
|
||
|
|
||
|
if result['shared_result'] and not isinstance(result['shared_result'], str):
|
||
|
msg = 'You can only provide a string type of value for "shared_result"'
|
||
|
raise pytest.Exception(msg)
|
||
|
return result
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
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
|
||
|
"""
|
||
|
args, kwargs = app_params
|
||
|
app_ = make_app(*args, **kwargs)
|
||
|
yield app_
|
||
|
|
||
|
print('# testroot:', kwargs.get('testroot', 'root'))
|
||
|
print('# builder:', app_.builder.name)
|
||
|
print('# srcdir:', app_.srcdir)
|
||
|
print('# outdir:', app_.outdir)
|
||
|
print('# status:', '\n' + app_._status.getvalue())
|
||
|
print('# warning:', '\n' + app_._warning.getvalue())
|
||
|
|
||
|
if test_params['shared_result']:
|
||
|
shared_result.store(test_params['shared_result'], app_)
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def status(app: SphinxTestApp) -> StringIO:
|
||
|
"""
|
||
|
Back-compatibility for testing with previous @with_app decorator
|
||
|
"""
|
||
|
return app._status
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def warning(app: SphinxTestApp) -> StringIO:
|
||
|
"""
|
||
|
Back-compatibility for testing with previous @with_app decorator
|
||
|
"""
|
||
|
return app._warning
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def make_app(test_params: dict, monkeypatch: Any) -> Generator[Callable, None, None]:
|
||
|
"""
|
||
|
Provides make_app function to initialize SphinxTestApp instance.
|
||
|
if you want to initialize 'app' in your test function. please use this
|
||
|
instead of using SphinxTestApp class directory.
|
||
|
"""
|
||
|
apps = []
|
||
|
syspath = sys.path[:]
|
||
|
|
||
|
def make(*args, **kwargs):
|
||
|
status, warning = StringIO(), StringIO()
|
||
|
kwargs.setdefault('status', status)
|
||
|
kwargs.setdefault('warning', warning)
|
||
|
app_: Any = SphinxTestApp(*args, **kwargs)
|
||
|
apps.append(app_)
|
||
|
if test_params['shared_result']:
|
||
|
app_ = SphinxTestAppWrapperForSkipBuilding(app_)
|
||
|
return app_
|
||
|
yield make
|
||
|
|
||
|
sys.path[:] = syspath
|
||
|
for app_ in reversed(apps): # clean up applications from the new ones
|
||
|
app_.cleanup()
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def shared_result() -> SharedResult:
|
||
|
return SharedResult()
|
||
|
|
||
|
|
||
|
@pytest.fixture(scope='module', autouse=True)
|
||
|
def _shared_result_cache() -> None:
|
||
|
SharedResult.cache.clear()
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def if_graphviz_found(app: SphinxTestApp) -> None: # NoQA: PT004
|
||
|
"""
|
||
|
The test will be skipped when using 'if_graphviz_found' fixture and graphviz
|
||
|
dot command is not found.
|
||
|
"""
|
||
|
graphviz_dot = getattr(app.config, 'graphviz_dot', '')
|
||
|
try:
|
||
|
if graphviz_dot:
|
||
|
subprocess.run([graphviz_dot, '-V'], capture_output=True) # show version
|
||
|
return
|
||
|
except OSError: # No such file or directory
|
||
|
pass
|
||
|
|
||
|
pytest.skip('graphviz "dot" is not available')
|
||
|
|
||
|
|
||
|
@pytest.fixture(scope='session')
|
||
|
def sphinx_test_tempdir(tmp_path_factory: Any) -> Path:
|
||
|
"""Temporary directory."""
|
||
|
return tmp_path_factory.getbasetemp()
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def rollback_sysmodules(): # NoQA: PT004
|
||
|
"""
|
||
|
Rollback sys.modules to its value before testing to unload modules
|
||
|
during tests.
|
||
|
|
||
|
For example, used in test_ext_autosummary.py to permit unloading the
|
||
|
target module to clear its cache.
|
||
|
"""
|
||
|
try:
|
||
|
sysmodules = list(sys.modules)
|
||
|
yield
|
||
|
finally:
|
||
|
for modname in list(sys.modules):
|
||
|
if modname not in sysmodules:
|
||
|
sys.modules.pop(modname)
|