From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/testing/fixtures.py | 243 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 sphinx/testing/fixtures.py (limited to 'sphinx/testing/fixtures.py') 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) -- cgit v1.2.3