Use read-only test roots (#13285)

This commit is contained in:
Adam Turner 2025-01-31 18:04:36 +00:00 committed by GitHub
parent d24ffe2949
commit 0d4425ce07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 89 additions and 25 deletions

View File

@ -49,6 +49,10 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Mount the test roots as read-only
run: |
mkdir -p ./tests/roots-read-only
sudo mount -v --bind --read-only ./tests/roots ./tests/roots-read-only
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:

View File

@ -104,12 +104,16 @@ def app_params(
test_root = kwargs.pop('testroot', 'root')
kwargs['srcdir'] = srcdir = sphinx_test_tempdir / kwargs.get('srcdir', test_root)
copy_test_root = not {'srcdir', 'copy_test_root'}.isdisjoint(kwargs)
# special support for sphinx/tests
if rootdir is not None:
test_root_path = rootdir / f'test-{test_root}'
if test_root_path.is_dir() and not srcdir.exists():
shutil.copytree(test_root_path, srcdir)
if copy_test_root:
if test_root_path.is_dir():
shutil.copytree(test_root_path, srcdir, dirs_exist_ok=True)
else:
kwargs['srcdir'] = test_root_path
# always write to the temporary directory
kwargs.setdefault('builddir', srcdir / '_build')

View File

@ -223,7 +223,11 @@ class SphinxTestApp(sphinx.application.Sphinx):
def cleanup(self, doctrees: bool = False) -> None:
sys.path[:] = self._saved_path
_clean_up_global_state()
self.docutils_conf_path.unlink(missing_ok=True)
try:
self.docutils_conf_path.unlink(missing_ok=True)
except OSError as exc:
if exc.errno != 30: # Ignore "read-only file system" errors
raise
def __repr__(self) -> str:
return f'<{self.__class__.__name__} buildername={self._builder_name!r}>'

View File

@ -19,7 +19,10 @@ if TYPE_CHECKING:
from collections.abc import Iterator
_TESTS_ROOT = Path(__file__).resolve().parent
_ROOTS_DIR = _TESTS_ROOT / 'roots'
if 'CI' in os.environ and (_TESTS_ROOT / 'roots-read-only').is_dir():
_ROOTS_DIR = _TESTS_ROOT / 'roots-read-only'
else:
_ROOTS_DIR = _TESTS_ROOT / 'roots'
def _init_console(

View File

@ -257,6 +257,7 @@ def test_too_many_retries(app: SphinxTestApp) -> None:
'linkcheck',
testroot='linkcheck-raw-node',
freshenv=True,
copy_test_root=True,
)
def test_raw_node(app: SphinxTestApp) -> None:
with serve_application(app, OKHandler) as address:

View File

@ -20,7 +20,7 @@ from sphinx.environment import (
)
@pytest.mark.sphinx('dummy', testroot='basic')
@pytest.mark.sphinx('dummy', testroot='basic', copy_test_root=True)
def test_config_status(make_app, app_params):
args, kwargs = app_params

View File

@ -1033,6 +1033,7 @@ def test_autodoc_typehints_description(app):
'autodoc_typehints': 'description',
'autodoc_typehints_description_target': 'documented',
},
copy_test_root=True,
)
def test_autodoc_typehints_description_no_undoc(app):
# No :type: or :rtype: will be injected for `incr`, which does not have
@ -1085,6 +1086,7 @@ def test_autodoc_typehints_description_no_undoc(app):
'autodoc_typehints': 'description',
'autodoc_typehints_description_target': 'documented_params',
},
copy_test_root=True,
)
def test_autodoc_typehints_description_no_undoc_doc_rtype(app):
# No :type: will be injected for `incr`, which does not have a description
@ -1154,6 +1156,7 @@ def test_autodoc_typehints_description_no_undoc_doc_rtype(app):
'text',
testroot='ext-autodoc',
confoverrides={'autodoc_typehints': 'description'},
copy_test_root=True,
)
def test_autodoc_typehints_description_with_documented_init(app):
with overwrite_file(
@ -1198,6 +1201,7 @@ def test_autodoc_typehints_description_with_documented_init(app):
'autodoc_typehints': 'description',
'autodoc_typehints_description_target': 'documented',
},
copy_test_root=True,
)
def test_autodoc_typehints_description_with_documented_init_no_undoc(app):
with overwrite_file(
@ -1232,6 +1236,7 @@ def test_autodoc_typehints_description_with_documented_init_no_undoc(app):
'autodoc_typehints': 'description',
'autodoc_typehints_description_target': 'documented_params',
},
copy_test_root=True,
)
def test_autodoc_typehints_description_with_documented_init_no_undoc_doc_rtype(app):
# see test_autodoc_typehints_description_with_documented_init_no_undoc
@ -1276,6 +1281,7 @@ def test_autodoc_typehints_description_for_invalid_node(app):
'text',
testroot='ext-autodoc',
confoverrides={'autodoc_typehints': 'both'},
copy_test_root=True,
)
def test_autodoc_typehints_both(app):
with overwrite_file(

View File

@ -151,6 +151,7 @@ def test_extract_summary(capsys):
'dummy',
testroot='ext-autosummary-ext',
confoverrides=defaults.copy(),
copy_test_root=True,
)
def test_get_items_summary(make_app, app_params):
import sphinx.ext.autosummary
@ -227,6 +228,7 @@ def str_content(elem: Element) -> str:
'xml',
testroot='ext-autosummary-ext',
confoverrides=defaults.copy(),
copy_test_root=True,
)
def test_escaping(app):
app.build(force_all=True)
@ -238,7 +240,7 @@ def test_escaping(app):
assert str_content(title) == 'underscore_module_'
@pytest.mark.sphinx('html', testroot='ext-autosummary')
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate_content_for_module(app):
import autosummary_dummy_module # type: ignore[import-not-found]
@ -298,7 +300,7 @@ def test_autosummary_generate_content_for_module(app):
assert context['objtype'] == 'module'
@pytest.mark.sphinx('html', testroot='ext-autosummary')
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate_content_for_module___all__(app):
import autosummary_dummy_module
@ -343,7 +345,7 @@ def test_autosummary_generate_content_for_module___all__(app):
assert context['objtype'] == 'module'
@pytest.mark.sphinx('html', testroot='ext-autosummary')
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate_content_for_module_skipped(app):
import autosummary_dummy_module
@ -389,7 +391,7 @@ def test_autosummary_generate_content_for_module_skipped(app):
assert context['exceptions'] == []
@pytest.mark.sphinx('html', testroot='ext-autosummary')
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate_content_for_module_imported_members(app):
import autosummary_dummy_module
@ -455,7 +457,7 @@ def test_autosummary_generate_content_for_module_imported_members(app):
assert context['objtype'] == 'module'
@pytest.mark.sphinx('html', testroot='ext-autosummary')
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate_content_for_module_imported_members_inherited_module(app):
import autosummary_dummy_inherited_module # type: ignore[import-not-found]
@ -501,7 +503,7 @@ def test_autosummary_generate_content_for_module_imported_members_inherited_modu
assert context['objtype'] == 'module'
@pytest.mark.sphinx('dummy', testroot='ext-autosummary')
@pytest.mark.sphinx('dummy', testroot='ext-autosummary', copy_test_root=True)
def test_autosummary_generate(app):
app.build(force_all=True)
@ -650,6 +652,7 @@ def test_autosummary_generate(app):
'dummy',
testroot='ext-autosummary',
confoverrides={'autosummary_generate_overwrite': False},
copy_test_root=True,
)
def test_autosummary_generate_overwrite1(app_params, make_app):
args, kwargs = app_params
@ -669,6 +672,7 @@ def test_autosummary_generate_overwrite1(app_params, make_app):
'dummy',
testroot='ext-autosummary',
confoverrides={'autosummary_generate_overwrite': True},
copy_test_root=True,
)
def test_autosummary_generate_overwrite2(app_params, make_app):
args, kwargs = app_params
@ -684,7 +688,7 @@ def test_autosummary_generate_overwrite2(app_params, make_app):
assert 'autosummary_dummy_module.rst' not in app._warning.getvalue()
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive')
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive', copy_test_root=True)
@pytest.mark.usefixtures('rollback_sysmodules')
def test_autosummary_recursive(app):
sys.modules.pop('package', None) # unload target module to clear the module cache
@ -738,7 +742,11 @@ def test_autosummary_recursive_skips_mocked_modules(app):
assert not (app.srcdir / 'generated' / 'package.package.module.rst').exists()
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-filename-map',
copy_test_root=True,
)
def test_autosummary_filename_map(app):
app.build()
@ -756,6 +764,7 @@ def test_autosummary_filename_map(app):
'latex',
testroot='ext-autosummary-ext',
confoverrides=defaults.copy(),
copy_test_root=True,
)
def test_autosummary_latex_table_colspec(app):
app.build(force_all=True)
@ -793,7 +802,11 @@ def test_import_by_name():
assert modname == 'sphinx.ext.autosummary'
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-mock_imports')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-mock_imports',
copy_test_root=True,
)
def test_autosummary_mock_imports(app):
try:
app.build()
@ -805,7 +818,11 @@ def test_autosummary_mock_imports(app):
sys.modules.pop('foo', None) # unload foo module
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-imported_members')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-imported_members',
copy_test_root=True,
)
def test_autosummary_imported_members(app):
try:
app.build()
@ -820,7 +837,11 @@ def test_autosummary_imported_members(app):
sys.modules.pop('autosummary_dummy_package', None)
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_all')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-module_all',
copy_test_root=True,
)
def test_autosummary_module_all(app):
try:
app.build()
@ -839,7 +860,11 @@ def test_autosummary_module_all(app):
sys.modules.pop('autosummary_dummy_package_all', None)
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_empty_all')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-module_empty_all',
copy_test_root=True,
)
def test_autosummary_module_empty_all(app):
try:
app.build()
@ -867,6 +892,7 @@ def test_autosummary_module_empty_all(app):
'html',
testroot='ext-autodoc',
confoverrides={'extensions': ['sphinx.ext.autosummary']},
copy_test_root=True,
)
def test_generate_autosummary_docs_property(app):
with patch('sphinx.ext.autosummary.generate.find_autosummary_in_files') as mock:
@ -886,7 +912,11 @@ def test_generate_autosummary_docs_property(app):
)
@pytest.mark.sphinx('html', testroot='ext-autosummary-skip-member')
@pytest.mark.sphinx(
'html',
testroot='ext-autosummary-skip-member',
copy_test_root=True,
)
def test_autosummary_skip_member(app):
app.build()
@ -895,7 +925,7 @@ def test_autosummary_skip_member(app):
assert 'Foo._privatemeth' in content
@pytest.mark.sphinx('html', testroot='ext-autosummary-template')
@pytest.mark.sphinx('html', testroot='ext-autosummary-template', copy_test_root=True)
def test_autosummary_template(app):
app.build()

View File

@ -59,7 +59,11 @@ def test_autosummary_import_cycle(app):
assert expected in app.warning.getvalue()
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_prefix')
@pytest.mark.sphinx(
'dummy',
testroot='ext-autosummary-module_prefix',
copy_test_root=True,
)
@pytest.mark.usefixtures('rollback_sysmodules')
def test_autosummary_generate_prefixes(app):
app.build()

View File

@ -696,7 +696,7 @@ def test_inspect_main_url(capsys):
assert stderr == ''
@pytest.mark.sphinx('html', testroot='ext-intersphinx-role')
@pytest.mark.sphinx('html', testroot='ext-intersphinx-role', copy_test_root=True)
def test_intersphinx_role(app):
inv_file = app.srcdir / 'inventory'
inv_file.write_bytes(INVENTORY_V2)

View File

@ -699,7 +699,7 @@ def test_gettext_buildr_ignores_only_directive(app):
@sphinx_intl
@pytest.mark.sphinx('html', testroot='intl')
@pytest.mark.sphinx('html', testroot='intl', copy_test_root=True)
def test_node_translated_attribute(app):
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
@ -713,7 +713,7 @@ def test_node_translated_attribute(app):
@sphinx_intl
@pytest.mark.sphinx('html', testroot='intl')
@pytest.mark.sphinx('html', testroot='intl', copy_test_root=True)
def test_translation_progress_substitution(app):
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
@ -732,6 +732,7 @@ def test_translation_progress_substitution(app):
'gettext_compact': False,
'translation_progress_classes': True,
},
copy_test_root=True,
)
def test_translation_progress_classes_true(app):
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
@ -862,6 +863,7 @@ def mock_time_and_i18n() -> Iterator[tuple[pytest.MonkeyPatch, _MockClock]]:
'dummy',
testroot='builder-gettext-dont-rebuild-mo',
freshenv=True,
copy_test_root=True,
)
def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
mock, clock = mock_time_and_i18n
@ -924,6 +926,7 @@ def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
'gettext',
testroot='builder-gettext-dont-rebuild-mo',
freshenv=True,
copy_test_root=True,
)
def test_gettext_dont_rebuild_mo(mock_time_and_i18n, app):
mock, clock = mock_time_and_i18n
@ -1677,6 +1680,7 @@ def test_additional_targets_should_be_translated(app):
'image',
],
},
copy_test_root=True,
)
def test_additional_targets_should_be_translated_substitution_definitions(app):
app.build(force_all=True)
@ -1713,6 +1717,7 @@ def test_text_references(app):
'locale_dirs': ['.'],
'gettext_compact': False,
},
copy_test_root=True,
)
def test_text_prolog_epilog_substitution(app):
app.build()
@ -1946,6 +1951,7 @@ def test_gettext_disallow_fuzzy_translations(app):
'html',
testroot='basic',
confoverrides={'language': 'de', 'html_sidebars': {'**': ['searchbox.html']}},
copy_test_root=True,
)
def test_customize_system_message(make_app, app_params):
try:

View File

@ -7,7 +7,7 @@ import pytest
from sphinx.ext.autosummary.generate import setup_documenters
@pytest.mark.sphinx('html', testroot='templating')
@pytest.mark.sphinx('html', testroot='templating', copy_test_root=True)
def test_layout_overloading(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
@ -18,7 +18,7 @@ def test_layout_overloading(make_app, app_params):
assert '<!-- layout overloading -->' in result
@pytest.mark.sphinx('html', testroot='templating')
@pytest.mark.sphinx('html', testroot='templating', copy_test_root=True)
def test_autosummary_class_template_overloading(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
@ -36,6 +36,7 @@ def test_autosummary_class_template_overloading(make_app, app_params):
'html',
testroot='templating',
confoverrides={'autosummary_context': {'sentence': 'foobar'}},
copy_test_root=True,
)
def test_autosummary_context(make_app, app_params):
args, kwargs = app_params

View File

@ -29,6 +29,7 @@ def test_html_with_default_docutilsconf(app):
testroot='docutilsconf',
freshenv=True,
docutils_conf='[restructuredtext parser]\ntrim_footnote_reference_space: true\n',
copy_test_root=True,
)
def test_html_with_docutilsconf(app):
with patch_docutils(app.confdir):