summaryrefslogtreecommitdiffstats
path: root/sphinx/testing/fixtures.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/testing/fixtures.py')
-rw-r--r--sphinx/testing/fixtures.py243
1 files changed, 243 insertions, 0 deletions
diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py
new file mode 100644
index 0000000..0cc4882
--- /dev/null
+++ b/sphinx/testing/fixtures.py
@@ -0,0 +1,243 @@
+"""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)