Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ repos:
# Read-only structural checks. Nothing here rewrites a file.
#
# trailing-whitespace and end-of-file-fixer are intentionally absent: the
# twister and Doxygen fixtures under sphinx/_extensions/_tests/fixtures/ are
# twister and Doxygen fixtures under sphinx/_fixtures/ are
# hand-crafted and compared byte for byte, and reStructuredText gives trailing
# whitespace meaning in line blocks. Both hooks would edit those files.
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ target-version = "py310"
exclude = [
"doc/_build",
"sphinx/_extensions/_tests/roots/*/_build",
"sphinx/_scripts/_tests/roots/*/_build",
]

[lint]
Expand Down
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ non-Python ``dot`` (Graphviz) dependency the architecture diagrams need.
Tests
-----

Two suites, in two repositories. The engine's own unit suite covers the
pure-Python extensions and runs in about a second::
Two suites, in two repositories. The engine's own unit suites cover the
Sphinx extensions (``sphinx/_extensions/_tests``) and the plain modules they
share (``sphinx/_scripts/_tests``). Both run in a few seconds; ``pytest.ini``
selects them::

$ python3 -m pytest sphinx/_extensions/_tests -q
$ python3 -m pytest -q

The acceptance suite lives in a separate project that consumes zdocs the way a
real one does, builds real documentation with real tools, and asserts on
Expand Down
1 change: 1 addition & 0 deletions doc/api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ENGINE_ROOT = Path(os.environ["ZDOCS_CONF_DIR"]).resolve().parent

sys.path.insert(0, str(ENGINE_ROOT / "sphinx" / "_extensions"))
sys.path.insert(0, str(ENGINE_ROOT / "sphinx" / "_scripts"))
sys.path.insert(0, str(ENGINE_ROOT / "scripts"))

configure(
Expand Down
52 changes: 2 additions & 50 deletions doc/api/python/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ Sphinx extensions
==================

Reference documentation for the modules under ``sphinx/_extensions/``: directives, roles and
builders loaded by :py:func:`zdocs_conf.configure`. See the companion
builders loaded by :py:func:`zdocs_conf.configure`. The plain modules they share are on
:doc:`support-modules`. See the companion
manual's :external+zdocs-manual:doc:`Directives and roles <reference/directives-and-roles>`
page for how a document author uses each one.

Expand Down Expand Up @@ -81,59 +82,10 @@ Re-reads a document when its sphinx-needs TOML file or the needs it imports chan
.. automodule:: needs_config_state
:members:

``needs_fields``
------------------

Which optional need fields a consumer declared.

.. automodule:: needs_fields
:members:

``xref_builder``
------------------

The stage-1 ``xref`` builder for the two-stage documentation build.

.. automodule:: xref_builder
:members:

``doxygen_parser``
--------------------

Doxygen XML parsing, with no Sphinx dependency of its own.

.. automodule:: doxygen_parser
:members:

``rst_builders``
------------------

RST string builders, with no Sphinx dependency of their own.

.. automodule:: rst_builders
:members:

``twister_reader``
--------------------

Twister output parsing, with no Sphinx dependency of its own.

.. automodule:: twister_reader
:members:
:exclude-members: parse_twister_results

``adequacy``
--------------

Coverage adequacy, with no Sphinx dependency of its own.

.. automodule:: adequacy
:members:

..
parse_twister_results is excluded: its docstring's own example text,
"leading 'test_' prefix stripped", ends in a bare trailing underscore
directly after a word — reST's own "quoted hyperlink reference" syntax
(``word_``) — with no matching target, so autodoc turns it into
"Unknown target name: 'test'". A source defect, not a directive-option
fix; see the final report.
1 change: 1 addition & 0 deletions doc/api/python/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ system. The API's for all of this can be found in the sub-sections below.
:maxdepth: 1

extensions
support-modules
scripts
conf
55 changes: 55 additions & 0 deletions doc/api/python/support-modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Support modules
================

The plain Python modules that the Sphinx extensions share: parsers, RST
builders and the coverage logic. None of them is a Sphinx extension; none has
a ``setup()``. Source: ``sphinx/_scripts/``.

``needs_fields``
------------------

Which optional need fields a consumer declared.

.. automodule:: needs_fields
:members:

``doxygen_parser``
--------------------

Doxygen XML parsing, with no Sphinx dependency of its own.

.. automodule:: doxygen_parser
:members:

``rst_builders``
------------------

RST string builders, with no Sphinx dependency of their own.

.. automodule:: rst_builders
:members:

``twister_reader``
--------------------

Twister output parsing, with no Sphinx dependency of its own.

.. automodule:: twister_reader
:members:
:exclude-members: parse_twister_results

``adequacy``
--------------

Coverage adequacy, with no Sphinx dependency of its own.

.. automodule:: adequacy
:members:

..
parse_twister_results is excluded: its docstring's own example text,
"leading 'test_' prefix stripped", ends in a bare trailing underscore
directly after a word — reST's own "quoted hyperlink reference" syntax
(``word_``) — with no matching target, so autodoc turns it into
"Unknown target name: 'test'". A source defect, not a directive-option
fix; see the final report.
11 changes: 8 additions & 3 deletions doc/manual/explanation/architecture/building-blocks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ follows the tool each area serves — see
subgraph cluster_sphinx {
label="sphinx/ — payload";
conf [label="zdocs_conf.py\nshared configure()"];
exts [label="_extensions/\nxref_builder, doc_control,\nqms_ref, latexinclude,\ntest_module + parsers"];
exts [label="_extensions/\nxref_builder, doc_control,\nqms_ref, latexinclude,\ntest_module, test_coverage"];
libs [label="_scripts/\nparsers, RST builders,\ncoverage adequacy"];
tmpl [label="_templates/, _static/"];
}

Expand All @@ -57,6 +58,7 @@ follows the tool each area serves — see
sphinx_cm -> conf [label="ZDOCS_CONF_DIR"];
conf -> docrefs [label="imports"];
conf -> exts;
exts -> libs [label="imports"];
conf -> tmpl;
doxygen_cm -> theme [label="HTML_EXTRA_*"];
doxygen_cm -> nav;
Expand Down Expand Up @@ -93,8 +95,11 @@ whole configuration, including the engine's extension list, and appends the
consumer's own extensions rather than being replaced by them. The extensions
divide into presentation (``doc_control``, ``qms_ref``, ``latexinclude``),
build machinery (``xref_builder``, which provides the stage-one index builder)
and the test-specification block (``test_module`` with its Doxygen XML parser,
twister reader and rST builders).
and the test-specification block (``test_module``, ``test_coverage``,
``symbol_needs``). ``_extensions/`` holds only Sphinx extensions, each with a
``setup()``. The plain modules they share are in ``_scripts/``: the Doxygen
XML parser, the twister reader, the rST builders and the coverage adequacy
logic.

``doxygen/`` — the Doxygen payload
----------------------------------
Expand Down
7 changes: 6 additions & 1 deletion doc/manual/explanation/decisions/0003-engine-unit-suite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ Decision
The engine carries a **second, fast unit suite in its own repository**, at
``sphinx/_extensions/_tests/``, covering the pure-Python extension modules.

*Update 2026-10-09:* the plain modules that the extensions share moved to
``sphinx/_scripts/``, with their tests in ``sphinx/_scripts/_tests/``. The
fixtures of both moved to ``sphinx/_fixtures/``. ``pytest.ini`` at the
repository root selects both folders.

There are therefore two suites in two repositories:

.. code-block:: console

$ cd tools/zdocs && python3 -m pytest sphinx/_extensions/_tests -q # fast
$ cd tools/zdocs && python3 -m pytest -q # fast
$ cd zdocs-tests && python3 -m pytest tests/ -q # acceptance

Neither layer can contaminate the other's count, and the engine becomes
Expand Down
10 changes: 6 additions & 4 deletions doc/manual/howto/run-the-test-suites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ In the zdocs repository, no build tree and no west workspace required:

.. code-block:: console

$ python3 -m pytest sphinx/_extensions/_tests -q
$ python3 -m pytest -q

Pure-Python tests over the Sphinx extensions (``doxygen_parser``,
``rst_builders``, ``twister_reader``, ``test_module``, and their need-type
mapping) run in-process against fixture XML/JSON — no Sphinx build, no
``pytest.ini`` selects two folders. ``sphinx/_scripts/_tests`` tests the plain
modules (``doxygen_parser``, ``rst_builders``, ``twister_reader``).
``sphinx/_extensions/_tests`` tests the Sphinx extensions (``test_module`` and
its need-type mapping, ``test_coverage``, ``doc_control``, ...). To run one,
give its folder. The tests run in-process against fixture XML/JSON — no Sphinx build, no
Doxygen, no CMake. It runs in about a second, which is what makes it the right
tool for iterating on extension logic; it proves nothing about CMake wiring or
about what actually lands in rendered HTML.
Expand Down
13 changes: 13 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0

# The engine's unit suites: sphinx/_scripts/_tests and sphinx/_extensions/_tests.
# The sphinx.testing plugin is loaded here and not in a conftest.py: pytest
# accepts `pytest_plugins` only in a top-level conftest, and with two suites
# neither conftest is top-level in a run of both.
[pytest]
addopts = -p sphinx.testing.fixtures
testpaths =
sphinx/_scripts/_tests
sphinx/_extensions/_tests
14 changes: 8 additions & 6 deletions sphinx/_extensions/_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

import pytest

# Make _extensions/ importable without install
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

FIXTURES = Path(__file__).parent / "fixtures"

pytest_plugins = "sphinx.testing.fixtures"
# Make _extensions/ importable without install, and _scripts/: the extensions
# import the modules there.
_SPHINX = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_SPHINX / "_scripts"))
sys.path.insert(0, str(_SPHINX / "_extensions"))

# The sphinx.testing fixtures (app, make_app, warning) come from pytest.ini at
# the repository root, so both suites can run in one pytest call.
# rootdir is intentionally left as the sphinx.testing default (None).
# Tests pass srcdir as an absolute path so Sphinx uses the source tree directly
# without copying, keeping __file__-relative paths in conf.py correct.
Expand Down
2 changes: 1 addition & 1 deletion sphinx/_extensions/_tests/roots/test-symbolneeds/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "symbol_needs"]
master_doc = "index"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

# Point the testcase.yaml lookup at a directory that has none, so
# build_scenario_table() returns [] and these roots stay focused on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

# Point the testcase.yaml lookup at a directory that has none, so
# build_scenario_table() returns [] and these roots stay focused on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

# Point the testcase.yaml lookup at a directory that has none, so
# build_scenario_table() returns [] and these roots stay focused on the
Expand Down
2 changes: 1 addition & 1 deletion sphinx/_extensions/_tests/roots/test-testmodule/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

# Point the testcase.yaml lookup at a directory that has none, so
# build_scenario_table() returns [] and these roots stay focused on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "test_module"]
master_doc = "index"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "test_module"]
master_doc = "index"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "test_module"]
master_doc = "index"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "test_module"]
master_doc = "index"
Expand Down
2 changes: 1 addition & 1 deletion sphinx/_extensions/_tests/roots/test-testreport/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

_FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
_FIXTURES = Path(__file__).resolve().parents[4] / "_fixtures"

extensions = ["sphinx_needs", "test_module"]
master_doc = "index"
Expand Down
3 changes: 2 additions & 1 deletion sphinx/_extensions/_tests/test_kconfig_depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import pytest
import rst_builders as rb
import test_module as tm
from conftest import FIXTURES

FIXTURES = Path(__file__).resolve().parents[2] / "_fixtures"

_ROOTS = Path(__file__).parent / "roots"
XML = FIXTURES / "doxygen-kconfig"
Expand Down
3 changes: 2 additions & 1 deletion sphinx/_extensions/_tests/test_suite_qualifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@

import pytest
import test_module as tm
from conftest import FIXTURES
from twister_reader import load_spec_lookup

FIXTURES = Path(__file__).resolve().parents[2] / "_fixtures"

_ROOTS = Path(__file__).parent / "roots"
XML = FIXTURES / "doxygen-qualifier"
M1 = "group__m1____workqueue__api"
Expand Down
Loading