From f59091286eddebce79db65c48e7228be3e04958d Mon Sep 17 00:00:00 2001 From: Julien Schueller Date: Wed, 21 Oct 2026 11:38:49 +0200 Subject: [PATCH 1/5] tests: harden linkcheck case-sensitivity test against slow CI runners Raise linkcheck_timeout from 0.25s to 2s: on runners with few CPUs, a local test-server round-trip can stall for a few hundred milliseconds under full-suite load, producing a flaky 'timeout' status instead of 'working'. Also implement do_HEAD in CapitalisePathHandler so each URL needs a single round-trip instead of a 501 HEAD followed by a GET. --- tests/roots/test-linkcheck-case-check/conf.py | 4 +++- tests/test_builders/test_build_linkcheck.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/roots/test-linkcheck-case-check/conf.py b/tests/roots/test-linkcheck-case-check/conf.py index 0c319123c30..2f0bc0db369 100644 --- a/tests/roots/test-linkcheck-case-check/conf.py +++ b/tests/roots/test-linkcheck-case-check/conf.py @@ -1 +1,3 @@ -linkcheck_timeout = 0.25 +# Short but not too short: CI runners with few CPUs can stall +# a local test server for a few hundred milliseconds under load. +linkcheck_timeout = 2 diff --git a/tests/test_builders/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py index e37b8e6e713..a0f21e98cc8 100644 --- a/tests/test_builders/test_build_linkcheck.py +++ b/tests/test_builders/test_build_linkcheck.py @@ -1461,20 +1461,22 @@ class CapitalisePathHandler(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' - def do_GET(self) -> None: + def _respond(self) -> None: if self.path.islower(): # Redirect lowercase paths to uppercase versions self.send_response(301, 'Moved Permanently') self.send_header('Location', self.path.upper()) - self.send_header('Content-Length', '0') - self.end_headers() else: # Serve uppercase paths - content = b'ok\n\n' self.send_response(200, 'OK') - self.send_header('Content-Length', str(len(content))) - self.end_headers() - self.wfile.write(content) + self.send_header('Content-Length', '0') + self.end_headers() + + def do_GET(self) -> None: + self._respond() + + def do_HEAD(self) -> None: + self._respond() @pytest.mark.sphinx( From 363e4a3356c514e9935b2e3b75eb110fd67f4794 Mon Sep 17 00:00:00 2001 From: Julien Schueller Date: Wed, 21 Oct 2026 11:38:49 +0200 Subject: [PATCH 2/5] CI: install docutils HEAD from GitHub mirror instead of repo.or.cz repo.or.cz intermittently fails with TLS handshake errors, breaking the docutils-latest and LaTeX jobs. The docutils/docutils mirror tracks the same trunk and is far more reliable from GH runners. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31ee999ab6d..85004f0cd6a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -249,7 +249,7 @@ jobs: - name: Install dependencies run: uv pip install . --group test - name: Install Docutils' HEAD - run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils" + run: uv pip install "docutils @ git+https://github.com/docutils/docutils.git#subdirectory=docutils" - name: Test with pytest run: python -m pytest -n logical --dist=worksteal -vv env: @@ -313,7 +313,7 @@ jobs: - name: Install dependencies run: uv pip install . --group test - name: Install Docutils' HEAD - run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils" + run: uv pip install "docutils @ git+https://github.com/docutils/docutils.git#subdirectory=docutils" - name: Test with pytest run: python -m pytest -vv --durations 25 env: From 4ab46af3e23ebe50d8d283a018709f4c202f9f2c Mon Sep 17 00:00:00 2001 From: Julien Schueller Date: Wed, 21 Oct 2026 11:38:49 +0200 Subject: [PATCH 3/5] tests: give each markup-rubric test its own srcdir The html, latex, texinfo, and man rubric tests shared one testroot (and thus srcdir/env) with different builders. Whichever builder ran first on an xdist worker populated the pickled environment, so later tests skipped re-reading sources: the html test then missed the parse-time '"7" unknown' directive warning while still emitting the write-phase warning, failing test_html5_rubric at random. Separate source directories remove the sharing entirely, following the approach of sphinx-doc/sphinx#14691. --- tests/test_builders/test_build_html_5_output.py | 2 +- tests/test_builders/test_build_latex.py | 2 +- tests/test_builders/test_build_manpage.py | 2 +- tests/test_builders/test_build_texinfo.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py index 0d3589b1a3d..9ebdd0ed2a9 100644 --- a/tests/test_builders/test_build_html_5_output.py +++ b/tests/test_builders/test_build_html_5_output.py @@ -551,7 +551,7 @@ def test_html5_output( check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check) -@pytest.mark.sphinx('html', testroot='markup-rubric') +@pytest.mark.sphinx('html', testroot='markup-rubric', srcdir='markup-rubric-html') def test_html5_rubric(app: SphinxTestApp) -> None: def insert_invalid_rubric_heading_level( app: SphinxTestApp, diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 06d75596323..9081ee527fb 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -2284,7 +2284,7 @@ def test_one_parameter_per_line_without_trailing_comma(app: SphinxTestApp) -> No assert r'\sphinxparam{\DUrole{n}{f}}}}' in result -@pytest.mark.sphinx('latex', testroot='markup-rubric') +@pytest.mark.sphinx('latex', testroot='markup-rubric', srcdir='markup-rubric-latex') def test_latex_rubric(app: SphinxTestApp) -> None: app.build() content = (app.outdir / 'test.tex').read_text(encoding='utf8') diff --git a/tests/test_builders/test_build_manpage.py b/tests/test_builders/test_build_manpage.py index 59ee10cccf5..f2bef959186 100644 --- a/tests/test_builders/test_build_manpage.py +++ b/tests/test_builders/test_build_manpage.py @@ -100,7 +100,7 @@ def test_default_man_pages() -> None: assert default_man_pages(config) == expected -@pytest.mark.sphinx('man', testroot='markup-rubric') +@pytest.mark.sphinx('man', testroot='markup-rubric', srcdir='markup-rubric-man') def test_rubric(app: SphinxTestApp) -> None: app.build() content = (app.outdir / 'projectnamenotset.1').read_text(encoding='utf8') diff --git a/tests/test_builders/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py index 58a5ce320a5..86a95b8ee45 100644 --- a/tests/test_builders/test_build_texinfo.py +++ b/tests/test_builders/test_build_texinfo.py @@ -44,7 +44,7 @@ def test_texinfo(app: SphinxTestApp) -> None: raise AssertionError(msg) from exc -@pytest.mark.sphinx('texinfo', testroot='markup-rubric') +@pytest.mark.sphinx('texinfo', testroot='markup-rubric', srcdir='markup-rubric-texinfo') def test_texinfo_rubric(app: SphinxTestApp) -> None: app.build() From cf9d1287f06bb21c24d4f62e37a90d30e942792e Mon Sep 17 00:00:00 2001 From: DavidAG Date: Fri, 20 Mar 2026 06:54:49 +0100 Subject: [PATCH 4/5] Stabilize numfig warning test --- tests/test_builders/test_build_html_numfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py index 144d9958d0d..637199d5267 100644 --- a/tests/test_builders/test_build_html_numfig.py +++ b/tests/test_builders/test_build_html_numfig.py @@ -19,7 +19,6 @@ @pytest.mark.sphinx('html', testroot='numfig') -@pytest.mark.test_params(shared_result='test_build_html_numfig') def test_numfig_disabled_warn(app: SphinxTestApp) -> None: app.build() warnings = app.warning.getvalue() From f381159f057a7a7cd0abb49ed379662e1add419b Mon Sep 17 00:00:00 2001 From: Kilian Paul <81327374+Darlokt@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:37:47 +0200 Subject: [PATCH 5/5] Fix flaky test isolation Give the conditional missing-reference test its own source and build directory so its generated HTML cannot affect Python domain tests. Clear NO_COLOR and NO_COLOUR before forcing color in status iterator tests, making their expectations independent of the invoking environment. --- AUTHORS.rst | 1 + CHANGES.rst | 4 ++++ tests/test_transforms/test_transforms_post_transforms.py | 1 + tests/test_util/test_util_display.py | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index b702c64c0f9..581d1fd4e68 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -81,6 +81,7 @@ Contributors * Justus Magin -- napoleon improvements * Kazuya Take -- ``sphinx.testing.path`` bug fix * Kevin Dunn -- MathJax extension +* Kilian Paul -- Docutils compatibility, intl tests, and viewcode fixes * KINEBUCHI Tomohiko -- typing Sphinx as well as docutils * Kurt McKee -- documentation updates * Lars Hupfeldt Nielsen - OpenSSL FIPS mode md5 bug fix diff --git a/CHANGES.rst b/CHANGES.rst index d0be07b8788..907b257a5e5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,10 @@ Release 9.1.1 (released Oct 19, 2026) Bugs fixed ---------- +* #14690: Fix test isolation issues caused by shared test directories and + inherited ``NO_COLOR`` and ``NO_COLOUR`` environment variables. + Patch by Kilian Paul + * #14465: LaTeX: PDF build crash since LaTeX June 2026 release if tables are styled with ``'colorrows'`` (which is the default). Patch by Jean-François B. diff --git a/tests/test_transforms/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py index 96f94f04910..81e99dd4ff2 100644 --- a/tests/test_transforms/test_transforms_post_transforms.py +++ b/tests/test_transforms/test_transforms_post_transforms.py @@ -70,6 +70,7 @@ def missing_reference( @pytest.mark.sphinx( 'html', testroot='domain-py-python_use_unqualified_type_names', + srcdir='transforms-post_transforms-missing-reference-conditional', freshenv=True, ) def test_missing_reference_conditional_pending_xref(app: SphinxTestApp) -> None: diff --git a/tests/test_util/test_util_display.py b/tests/test_util/test_util_display.py index fce4ac1fbdf..f6f359efc8c 100644 --- a/tests/test_util/test_util_display.py +++ b/tests/test_util/test_util_display.py @@ -44,6 +44,8 @@ def test_status_iterator_length_0(app: SphinxTestApp) -> None: def test_status_iterator_verbosity_0( app: SphinxTestApp, monkeypatch: pytest.MonkeyPatch ) -> None: + monkeypatch.delenv('NO_COLOR', raising=False) + monkeypatch.delenv('NO_COLOUR', raising=False) monkeypatch.setenv('FORCE_COLOR', '1') logging.setup(app, app.status, app.warning) @@ -64,6 +66,8 @@ def test_status_iterator_verbosity_0( def test_status_iterator_verbosity_1( app: SphinxTestApp, monkeypatch: pytest.MonkeyPatch ) -> None: + monkeypatch.delenv('NO_COLOR', raising=False) + monkeypatch.delenv('NO_COLOUR', raising=False) monkeypatch.setenv('FORCE_COLOR', '1') logging.setup(app, app.status, app.warning)