Skip to content

Commit 0d4425c

Browse files
authoredJan 31, 2025
Use read-only test roots (sphinx-doc#13285)
1 parent d24ffe2 commit 0d4425c

File tree

13 files changed

+89
-25
lines changed

13 files changed

+89
-25
lines changed
 

‎.github/workflows/main.yml

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ jobs:
4949
- uses: actions/checkout@v4
5050
with:
5151
persist-credentials: false
52+
- name: Mount the test roots as read-only
53+
run: |
54+
mkdir -p ./tests/roots-read-only
55+
sudo mount -v --bind --read-only ./tests/roots ./tests/roots-read-only
5256
- name: Set up Python ${{ matrix.python }}
5357
uses: actions/setup-python@v5
5458
with:

‎sphinx/testing/fixtures.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,16 @@ def app_params(
104104

105105
test_root = kwargs.pop('testroot', 'root')
106106
kwargs['srcdir'] = srcdir = sphinx_test_tempdir / kwargs.get('srcdir', test_root)
107+
copy_test_root = not {'srcdir', 'copy_test_root'}.isdisjoint(kwargs)
107108

108109
# special support for sphinx/tests
109110
if rootdir is not None:
110111
test_root_path = rootdir / f'test-{test_root}'
111-
if test_root_path.is_dir() and not srcdir.exists():
112-
shutil.copytree(test_root_path, srcdir)
112+
if copy_test_root:
113+
if test_root_path.is_dir():
114+
shutil.copytree(test_root_path, srcdir, dirs_exist_ok=True)
115+
else:
116+
kwargs['srcdir'] = test_root_path
113117

114118
# always write to the temporary directory
115119
kwargs.setdefault('builddir', srcdir / '_build')

‎sphinx/testing/util.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,11 @@ def warning(self) -> StringIO:
223223
def cleanup(self, doctrees: bool = False) -> None:
224224
sys.path[:] = self._saved_path
225225
_clean_up_global_state()
226-
self.docutils_conf_path.unlink(missing_ok=True)
226+
try:
227+
self.docutils_conf_path.unlink(missing_ok=True)
228+
except OSError as exc:
229+
if exc.errno != 30: # Ignore "read-only file system" errors
230+
raise
227231

228232
def __repr__(self) -> str:
229233
return f'<{self.__class__.__name__} buildername={self._builder_name!r}>'

‎tests/conftest.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
from collections.abc import Iterator
2020

2121
_TESTS_ROOT = Path(__file__).resolve().parent
22-
_ROOTS_DIR = _TESTS_ROOT / 'roots'
22+
if 'CI' in os.environ and (_TESTS_ROOT / 'roots-read-only').is_dir():
23+
_ROOTS_DIR = _TESTS_ROOT / 'roots-read-only'
24+
else:
25+
_ROOTS_DIR = _TESTS_ROOT / 'roots'
2326

2427

2528
def _init_console(

‎tests/test_builders/test_build_linkcheck.py

+1
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def test_too_many_retries(app: SphinxTestApp) -> None:
257257
'linkcheck',
258258
testroot='linkcheck-raw-node',
259259
freshenv=True,
260+
copy_test_root=True,
260261
)
261262
def test_raw_node(app: SphinxTestApp) -> None:
262263
with serve_application(app, OKHandler) as address:

‎tests/test_environment/test_environment.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
)
2121

2222

23-
@pytest.mark.sphinx('dummy', testroot='basic')
23+
@pytest.mark.sphinx('dummy', testroot='basic', copy_test_root=True)
2424
def test_config_status(make_app, app_params):
2525
args, kwargs = app_params
2626

‎tests/test_extensions/test_ext_autodoc_configs.py

+6
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@ def test_autodoc_typehints_description(app):
10331033
'autodoc_typehints': 'description',
10341034
'autodoc_typehints_description_target': 'documented',
10351035
},
1036+
copy_test_root=True,
10361037
)
10371038
def test_autodoc_typehints_description_no_undoc(app):
10381039
# 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):
10851086
'autodoc_typehints': 'description',
10861087
'autodoc_typehints_description_target': 'documented_params',
10871088
},
1089+
copy_test_root=True,
10881090
)
10891091
def test_autodoc_typehints_description_no_undoc_doc_rtype(app):
10901092
# 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):
11541156
'text',
11551157
testroot='ext-autodoc',
11561158
confoverrides={'autodoc_typehints': 'description'},
1159+
copy_test_root=True,
11571160
)
11581161
def test_autodoc_typehints_description_with_documented_init(app):
11591162
with overwrite_file(
@@ -1198,6 +1201,7 @@ def test_autodoc_typehints_description_with_documented_init(app):
11981201
'autodoc_typehints': 'description',
11991202
'autodoc_typehints_description_target': 'documented',
12001203
},
1204+
copy_test_root=True,
12011205
)
12021206
def test_autodoc_typehints_description_with_documented_init_no_undoc(app):
12031207
with overwrite_file(
@@ -1232,6 +1236,7 @@ def test_autodoc_typehints_description_with_documented_init_no_undoc(app):
12321236
'autodoc_typehints': 'description',
12331237
'autodoc_typehints_description_target': 'documented_params',
12341238
},
1239+
copy_test_root=True,
12351240
)
12361241
def test_autodoc_typehints_description_with_documented_init_no_undoc_doc_rtype(app):
12371242
# see test_autodoc_typehints_description_with_documented_init_no_undoc
@@ -1276,6 +1281,7 @@ def test_autodoc_typehints_description_for_invalid_node(app):
12761281
'text',
12771282
testroot='ext-autodoc',
12781283
confoverrides={'autodoc_typehints': 'both'},
1284+
copy_test_root=True,
12791285
)
12801286
def test_autodoc_typehints_both(app):
12811287
with overwrite_file(

‎tests/test_extensions/test_ext_autosummary.py

+44-14
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def test_extract_summary(capsys):
151151
'dummy',
152152
testroot='ext-autosummary-ext',
153153
confoverrides=defaults.copy(),
154+
copy_test_root=True,
154155
)
155156
def test_get_items_summary(make_app, app_params):
156157
import sphinx.ext.autosummary
@@ -227,6 +228,7 @@ def str_content(elem: Element) -> str:
227228
'xml',
228229
testroot='ext-autosummary-ext',
229230
confoverrides=defaults.copy(),
231+
copy_test_root=True,
230232
)
231233
def test_escaping(app):
232234
app.build(force_all=True)
@@ -238,7 +240,7 @@ def test_escaping(app):
238240
assert str_content(title) == 'underscore_module_'
239241

240242

241-
@pytest.mark.sphinx('html', testroot='ext-autosummary')
243+
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
242244
def test_autosummary_generate_content_for_module(app):
243245
import autosummary_dummy_module # type: ignore[import-not-found]
244246

@@ -298,7 +300,7 @@ def test_autosummary_generate_content_for_module(app):
298300
assert context['objtype'] == 'module'
299301

300302

301-
@pytest.mark.sphinx('html', testroot='ext-autosummary')
303+
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
302304
def test_autosummary_generate_content_for_module___all__(app):
303305
import autosummary_dummy_module
304306

@@ -343,7 +345,7 @@ def test_autosummary_generate_content_for_module___all__(app):
343345
assert context['objtype'] == 'module'
344346

345347

346-
@pytest.mark.sphinx('html', testroot='ext-autosummary')
348+
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
347349
def test_autosummary_generate_content_for_module_skipped(app):
348350
import autosummary_dummy_module
349351

@@ -389,7 +391,7 @@ def skip_member(app, what, name, obj, skip, options):
389391
assert context['exceptions'] == []
390392

391393

392-
@pytest.mark.sphinx('html', testroot='ext-autosummary')
394+
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
393395
def test_autosummary_generate_content_for_module_imported_members(app):
394396
import autosummary_dummy_module
395397

@@ -455,7 +457,7 @@ def test_autosummary_generate_content_for_module_imported_members(app):
455457
assert context['objtype'] == 'module'
456458

457459

458-
@pytest.mark.sphinx('html', testroot='ext-autosummary')
460+
@pytest.mark.sphinx('html', testroot='ext-autosummary', copy_test_root=True)
459461
def test_autosummary_generate_content_for_module_imported_members_inherited_module(app):
460462
import autosummary_dummy_inherited_module # type: ignore[import-not-found]
461463

@@ -501,7 +503,7 @@ def test_autosummary_generate_content_for_module_imported_members_inherited_modu
501503
assert context['objtype'] == 'module'
502504

503505

504-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary')
506+
@pytest.mark.sphinx('dummy', testroot='ext-autosummary', copy_test_root=True)
505507
def test_autosummary_generate(app):
506508
app.build(force_all=True)
507509

@@ -650,6 +652,7 @@ def test_autosummary_generate(app):
650652
'dummy',
651653
testroot='ext-autosummary',
652654
confoverrides={'autosummary_generate_overwrite': False},
655+
copy_test_root=True,
653656
)
654657
def test_autosummary_generate_overwrite1(app_params, make_app):
655658
args, kwargs = app_params
@@ -669,6 +672,7 @@ def test_autosummary_generate_overwrite1(app_params, make_app):
669672
'dummy',
670673
testroot='ext-autosummary',
671674
confoverrides={'autosummary_generate_overwrite': True},
675+
copy_test_root=True,
672676
)
673677
def test_autosummary_generate_overwrite2(app_params, make_app):
674678
args, kwargs = app_params
@@ -684,7 +688,7 @@ def test_autosummary_generate_overwrite2(app_params, make_app):
684688
assert 'autosummary_dummy_module.rst' not in app._warning.getvalue()
685689

686690

687-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive')
691+
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive', copy_test_root=True)
688692
@pytest.mark.usefixtures('rollback_sysmodules')
689693
def test_autosummary_recursive(app):
690694
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):
738742
assert not (app.srcdir / 'generated' / 'package.package.module.rst').exists()
739743

740744

741-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map')
745+
@pytest.mark.sphinx(
746+
'dummy',
747+
testroot='ext-autosummary-filename-map',
748+
copy_test_root=True,
749+
)
742750
def test_autosummary_filename_map(app):
743751
app.build()
744752

@@ -756,6 +764,7 @@ def test_autosummary_filename_map(app):
756764
'latex',
757765
testroot='ext-autosummary-ext',
758766
confoverrides=defaults.copy(),
767+
copy_test_root=True,
759768
)
760769
def test_autosummary_latex_table_colspec(app):
761770
app.build(force_all=True)
@@ -793,7 +802,11 @@ def test_import_by_name():
793802
assert modname == 'sphinx.ext.autosummary'
794803

795804

796-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-mock_imports')
805+
@pytest.mark.sphinx(
806+
'dummy',
807+
testroot='ext-autosummary-mock_imports',
808+
copy_test_root=True,
809+
)
797810
def test_autosummary_mock_imports(app):
798811
try:
799812
app.build()
@@ -805,7 +818,11 @@ def test_autosummary_mock_imports(app):
805818
sys.modules.pop('foo', None) # unload foo module
806819

807820

808-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-imported_members')
821+
@pytest.mark.sphinx(
822+
'dummy',
823+
testroot='ext-autosummary-imported_members',
824+
copy_test_root=True,
825+
)
809826
def test_autosummary_imported_members(app):
810827
try:
811828
app.build()
@@ -820,7 +837,11 @@ def test_autosummary_imported_members(app):
820837
sys.modules.pop('autosummary_dummy_package', None)
821838

822839

823-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_all')
840+
@pytest.mark.sphinx(
841+
'dummy',
842+
testroot='ext-autosummary-module_all',
843+
copy_test_root=True,
844+
)
824845
def test_autosummary_module_all(app):
825846
try:
826847
app.build()
@@ -839,7 +860,11 @@ def test_autosummary_module_all(app):
839860
sys.modules.pop('autosummary_dummy_package_all', None)
840861

841862

842-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_empty_all')
863+
@pytest.mark.sphinx(
864+
'dummy',
865+
testroot='ext-autosummary-module_empty_all',
866+
copy_test_root=True,
867+
)
843868
def test_autosummary_module_empty_all(app):
844869
try:
845870
app.build()
@@ -867,6 +892,7 @@ def test_autosummary_module_empty_all(app):
867892
'html',
868893
testroot='ext-autodoc',
869894
confoverrides={'extensions': ['sphinx.ext.autosummary']},
895+
copy_test_root=True,
870896
)
871897
def test_generate_autosummary_docs_property(app):
872898
with patch('sphinx.ext.autosummary.generate.find_autosummary_in_files') as mock:
@@ -886,7 +912,11 @@ def test_generate_autosummary_docs_property(app):
886912
)
887913

888914

889-
@pytest.mark.sphinx('html', testroot='ext-autosummary-skip-member')
915+
@pytest.mark.sphinx(
916+
'html',
917+
testroot='ext-autosummary-skip-member',
918+
copy_test_root=True,
919+
)
890920
def test_autosummary_skip_member(app):
891921
app.build()
892922

@@ -895,7 +925,7 @@ def test_autosummary_skip_member(app):
895925
assert 'Foo._privatemeth' in content
896926

897927

898-
@pytest.mark.sphinx('html', testroot='ext-autosummary-template')
928+
@pytest.mark.sphinx('html', testroot='ext-autosummary-template', copy_test_root=True)
899929
def test_autosummary_template(app):
900930
app.build()
901931

‎tests/test_extensions/test_ext_autosummary_imports.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ def test_autosummary_import_cycle(app):
5959
assert expected in app.warning.getvalue()
6060

6161

62-
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_prefix')
62+
@pytest.mark.sphinx(
63+
'dummy',
64+
testroot='ext-autosummary-module_prefix',
65+
copy_test_root=True,
66+
)
6367
@pytest.mark.usefixtures('rollback_sysmodules')
6468
def test_autosummary_generate_prefixes(app):
6569
app.build()

‎tests/test_extensions/test_ext_intersphinx.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ def log_message(*args, **kwargs):
696696
assert stderr == ''
697697

698698

699-
@pytest.mark.sphinx('html', testroot='ext-intersphinx-role')
699+
@pytest.mark.sphinx('html', testroot='ext-intersphinx-role', copy_test_root=True)
700700
def test_intersphinx_role(app):
701701
inv_file = app.srcdir / 'inventory'
702702
inv_file.write_bytes(INVENTORY_V2)

‎tests/test_intl/test_intl.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ def test_gettext_buildr_ignores_only_directive(app):
699699

700700

701701
@sphinx_intl
702-
@pytest.mark.sphinx('html', testroot='intl')
702+
@pytest.mark.sphinx('html', testroot='intl', copy_test_root=True)
703703
def test_node_translated_attribute(app):
704704
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
705705

@@ -713,7 +713,7 @@ def test_node_translated_attribute(app):
713713

714714

715715
@sphinx_intl
716-
@pytest.mark.sphinx('html', testroot='intl')
716+
@pytest.mark.sphinx('html', testroot='intl', copy_test_root=True)
717717
def test_translation_progress_substitution(app):
718718
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
719719

@@ -732,6 +732,7 @@ def test_translation_progress_substitution(app):
732732
'gettext_compact': False,
733733
'translation_progress_classes': True,
734734
},
735+
copy_test_root=True,
735736
)
736737
def test_translation_progress_classes_true(app):
737738
app.build(filenames=[app.srcdir / 'translation_progress.txt'])
@@ -862,6 +863,7 @@ def mock_write_mo(self, locale, use_fuzzy=False):
862863
'dummy',
863864
testroot='builder-gettext-dont-rebuild-mo',
864865
freshenv=True,
866+
copy_test_root=True,
865867
)
866868
def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
867869
mock, clock = mock_time_and_i18n
@@ -924,6 +926,7 @@ def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
924926
'gettext',
925927
testroot='builder-gettext-dont-rebuild-mo',
926928
freshenv=True,
929+
copy_test_root=True,
927930
)
928931
def test_gettext_dont_rebuild_mo(mock_time_and_i18n, app):
929932
mock, clock = mock_time_and_i18n
@@ -1677,6 +1680,7 @@ def test_additional_targets_should_be_translated(app):
16771680
'image',
16781681
],
16791682
},
1683+
copy_test_root=True,
16801684
)
16811685
def test_additional_targets_should_be_translated_substitution_definitions(app):
16821686
app.build(force_all=True)
@@ -1713,6 +1717,7 @@ def test_text_references(app):
17131717
'locale_dirs': ['.'],
17141718
'gettext_compact': False,
17151719
},
1720+
copy_test_root=True,
17161721
)
17171722
def test_text_prolog_epilog_substitution(app):
17181723
app.build()
@@ -1946,6 +1951,7 @@ def test_gettext_disallow_fuzzy_translations(app):
19461951
'html',
19471952
testroot='basic',
19481953
confoverrides={'language': 'de', 'html_sidebars': {'**': ['searchbox.html']}},
1954+
copy_test_root=True,
19491955
)
19501956
def test_customize_system_message(make_app, app_params):
19511957
try:

‎tests/test_theming/test_templating.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sphinx.ext.autosummary.generate import setup_documenters
88

99

10-
@pytest.mark.sphinx('html', testroot='templating')
10+
@pytest.mark.sphinx('html', testroot='templating', copy_test_root=True)
1111
def test_layout_overloading(make_app, app_params):
1212
args, kwargs = app_params
1313
app = make_app(*args, **kwargs)
@@ -18,7 +18,7 @@ def test_layout_overloading(make_app, app_params):
1818
assert '<!-- layout overloading -->' in result
1919

2020

21-
@pytest.mark.sphinx('html', testroot='templating')
21+
@pytest.mark.sphinx('html', testroot='templating', copy_test_root=True)
2222
def test_autosummary_class_template_overloading(make_app, app_params):
2323
args, kwargs = app_params
2424
app = make_app(*args, **kwargs)
@@ -36,6 +36,7 @@ def test_autosummary_class_template_overloading(make_app, app_params):
3636
'html',
3737
testroot='templating',
3838
confoverrides={'autosummary_context': {'sentence': 'foobar'}},
39+
copy_test_root=True,
3940
)
4041
def test_autosummary_context(make_app, app_params):
4142
args, kwargs = app_params

‎tests/test_writers/test_docutilsconf.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_html_with_default_docutilsconf(app):
2929
testroot='docutilsconf',
3030
freshenv=True,
3131
docutils_conf='[restructuredtext parser]\ntrim_footnote_reference_space: true\n',
32+
copy_test_root=True,
3233
)
3334
def test_html_with_docutilsconf(app):
3435
with patch_docutils(app.confdir):

0 commit comments

Comments
 (0)
Please sign in to comment.