Sphinx extensions

Reference documentation for the modules under sphinx/_extensions/: directives, roles and builders loaded by zdocs_conf.configure(). See the companion manual’s Directives and roles page for how a document author uses each one.

doc_control

The .. doc_control:: directive — the controlled-document header table.

class doc_control.DocCtrlDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]
has_content = False

May the directive have content?

option_spec = {'approval_date': <function unchanged_required>, 'approved_by': <function unchanged_required>, 'author': <function unchanged_required>, 'based_on_template': <function unchanged>, 'classification': <function unchanged>, 'effective_date': <function unchanged>, 'owner': <function unchanged_required>, 'reviewed_by': <function unchanged_required>, 'supersedes': <function unchanged>, 'version': <function unchanged_required>}

Mapping of option names to validator functions.

doc_control.latex_escape(text)[source]

Escape text for use in a LaTeX string.

Public because zdocs_conf needs the same escaping for the running header and footer it builds out of consumer-supplied values (project name, copyright holder, document id). Those are the strings most likely to contain an & or an _, and a second copy of this table living in the config module is how the two would stop agreeing.

qms_ref

The .. qms_role:: directive.

class qms_ref.QmsDocRole[source]

latexinclude

The .. latexinclude:: directive.

The latexinclude directive — include an RST file in the PDF only.

.. latexinclude:: ../_glossary_terms.rst   # relative to the AUTHORED file
.. latexinclude:: /_glossary_terms.rst     # relative to the SOURCE TREE

A printed document carries no hyperlinks, so anything an HTML reader would reach by following a link has to travel with the PDF instead: a shared glossary, a terms-and-abbreviations appendix, a standards list. Inlining the same content into the HTML would just give that reader a second copy of a page they can already open, so this includes for LaTeX builders and does nothing everywhere else.

The path is relative to the including file as AUTHORED, which is not where Sphinx parses it from. external_content copies each document’s folder into <build>/<doc>/src and the build runs from that copy, so resolving the argument against the parse location sends .. into <build>/<doc> — a directory of doctrees and logs, never of authored RST. The only file worth including this way is a SHARED one (a file inside the document would simply be part of it), and a shared file is by definition outside the folder that gets copied. So the wrong base directory does not degrade this directive, it breaks every legitimate use of it.

The authored location is reconstructed from Sphinx’s own two facts: confdir is the document’s authored directory (cmake passes it as -c), and docname is the including file’s path within the document. Together they give the file the author was looking at when they counted the ...

A leading ``/`` means relative to the source tree instead, exactly as it does for Sphinx’s own include directive (“interprets absolute paths correctly, i.e. relative to source directory”). The same spelling then works from both directives and from any depth, which is the point: an authored-relative path encodes how far the including file sits below the shared file, so moving a document from doc/<id>/ to doc/<group>/<id>/ silently changes what ../ means and turns a working glossary into a build failure. A source-tree path says where the file IS, not how far away the author happened to be.

That spelling requires the shared file to be present in the source tree, which it is not by default – external_content copies only the document’s own folder. A consumer that wants it adds it to external_content_contents (and to exclude_patterns, since an include fragment is not a document). Both spellings are supported and neither is deprecated: the authored-relative one remains correct for a file that is deliberately NOT copied into the build.

class latexinclude.LatexIncludeDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Include an RST file for LaTeX builders, skipping it for every other.

has_content = False

May the directive have content?

required_arguments = 1

Number of required directive arguments.

optional_arguments = 0

Number of optional arguments after the required arguments.

final_argument_whitespace = False

May the final argument contain whitespace?

option_spec = {}

Mapping of option names to validator functions.

test_module

The .. testmodule::, .. testreport:: and .. twisterinfo:: directives.

Sphinx extension: testmodule and testreport directives (Route B — sphinx-needs).

test_module.suite_name_from_group(group_name, qualifier='')[source]

The ztest suite name a suite group’s compoundname stands for.

With a testmodule_suite_qualifier, the part after its LAST occurrence (kernel_workq_user_work_module__workqueue_api -> workqueue_api for "__"), so two test modules declaring the same ZTEST_SUITE can give it distinct Doxygen groups. Without a qualifier, without an occurrence of it, or with nothing after it, the whole name.

class test_module.TestModuleDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Emit sphinx-needs test_case nodes for all ZTEST functions in a module group.

Usage:

.. testmodule:: kernel_queue_module
   :module: tests/kernel/queue
required_arguments = 1

Number of required directive arguments.

optional_arguments = 0

Number of optional arguments after the required arguments.

has_content = False

May the directive have content?

option_spec = {'module': <function unchanged>}

Mapping of option names to validator functions.

class test_module.TestReportDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Emit sphinx-needs test_result nodes from a twister_report.xml.

Usage:

.. testreport:: twister_report.xml
   :path: tests/kernel/queue

:path: selects the runs of one test directory, as twister.json records it; :module: selects by scenario-name prefix. With both, a run must match both.

required_arguments = 1

Number of required directive arguments.

optional_arguments = 0

Number of optional arguments after the required arguments.

has_content = False

May the directive have content?

option_spec = {'module': <function unchanged>, 'path': <function unchanged>}

Mapping of option names to validator functions.

class test_module.TwisterInfoDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Emit a run-metadata block and per-platform summary table from twister.json.

Usage:

.. twisterinfo:: twister.json
required_arguments = 1

Number of required directive arguments.

optional_arguments = 0

Number of optional arguments after the required arguments.

has_content = False

May the directive have content?

option_spec = {}

Mapping of option names to validator functions.

test_coverage

The .. testcoverage:: directive. test_module loads it.

Sphinx extension: the testcoverage directive (coverage adequacy as needs).

.. testcoverage:: reads a per-test coverage run (adequacy) and emits one adequacy need per requirement that the run can assess. Each need links to its requirement (link role assesses) and carries the verdict. The directive also writes a summary of the run and the distribution of the verdicts.

test_coverage.ADEQUACY_FIELD_ROLES = ('verdict', 'evidence', 'coverage_run', 'judged_symbols', 'symbol_hits')

The adequacy field roles (testcoverage_need_fields).

test_coverage.VERDICT_MEANING = {'broken': "Tests of the run reach the code. The requirement's own tests never do.", 'no-cov': 'The verifying tests have no coverage data in this run.', 'no-impl': 'No symbol satisfies the requirement.', 'partial': 'The own tests run some of the satisfying symbols, not all.', 'true': 'The own tests run every symbol that coverage can judge.', 'unattributed': 'No test of the run covers any body. Coverage cannot judge the link.', 'unresolved': 'No satisfying symbol maps to a body (a macro).'}

What each verdict says, for the distribution table.

test_coverage.adequacy_need_id(run, req, prefix='ADQ')[source]

<prefix>-<run>/<req>, the id of req’s adequacy need in run.

test_coverage.build_adequacy_rst(req, res, run, need_names=None, fields=(), prefix='ADQ', layout='')[source]

RST lines for the adequacy need of req (res from adequacy.assess).

layout: the sphinx-needs layout of the need, if not empty.

test_coverage.build_coverage_rst(results, run, source, impl_loc, need_names=None, fields=(), prefix='ADQ', layout='', impl_files=('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h'))[source]

RST lines for the whole directive: run summary, distribution, needs by verdict.

impl_files: the body files that the run searched, for the summary.

class test_coverage.TestCoverageDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Emit one adequacy need per requirement that a per-test coverage run can assess.

Usage:

.. testcoverage::
   :run: my-run
   :layout: adequacy

The optional argument is the run directory. Without it, the directive reads coverage_output_dir (ZDOCS_COVERAGE_OUT). :run: names the run in the need ids. Without it, the name is the first tag on the run commit, or else the name of the run directory. :layout: sets the sphinx-needs layout of each need. testcoverage_impl_files sets the files that hold the bodies of the satisfying symbols.

required_arguments = 0

Number of required directive arguments.

optional_arguments = 1

Number of optional arguments after the required arguments.

has_content = False

May the directive have content?

option_spec = {'layout': <function unchanged>, 'run': <function unchanged>}

Mapping of option names to validator functions.

symbol_needs

The .. symbolneeds:: directive.

Sphinx extension: the symbolneeds directive — one need per API symbol that satisfies a requirement.

A Doxygen 1.16 \satisfies <UID> on a function or macro becomes, in the XML, a <satisfies> child of its memberdef. This directive turns each such symbol into a need (the implementation role) linked to those requirements (the satisfies role), so a requirement’s page shows what implements it next to what verifies it.

Loaded only for a document whose registry entry has a symbol_needs: block, which also names the Doxygen document whose XML is read.

symbol_needs.symbol_needs_rst(xml_dir, html_dir, group=None, need_names=None, note_input=None, depends_field=None)[source]

RST lines for the symbol needs of group (or the whole project).

With no group, the needs are sectioned by compound (group or file), each heading taken from the compound’s title. A member is emitted once, where Doxygen defines it, however many compounds list it. note_input is called with every XML file read. depends_field(conditions, subject) decides whether a need gets the depends_on field (needs_fields.depends_field); without it, none does.

class symbol_needs.SymbolNeedsDirective(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)[source]

Emit one need per API symbol carrying a Doxygen \satisfies.

Usage:

.. symbolneeds::

.. symbolneeds:: queue_apis

The optional argument is a Doxygen group name; without it every annotated symbol in the project is emitted, sectioned by group or file.

required_arguments = 0

Number of required directive arguments.

optional_arguments = 1

Number of optional arguments after the required arguments.

has_content = False

May the directive have content?

option_spec = {}

Mapping of option names to validator functions.

input_tracking

Re-reads a document when an input from outside the source tree changes.

Sphinx extension: track a document’s inputs from outside the source tree.

The testmodule, testreport, twisterinfo and symbolneeds directives read files Sphinx knows nothing about: Doxygen XML, twister’s XML/JSON, a spec’s needs.json. Sphinx re-reads a document only when a tracked input is missing or NEWER than the document’s last read. That is not enough here: twister output often arrives with an older mtime (a CI artifact or cache restored with its timestamps, a copy that preserves them), and the report then stays stale without a warning. So each input’s signature is recorded at read time, and a document is re-read whenever a signature differs, in either direction.

Loaded by the extensions that use it (app.setup_extension), which Sphinx does once however many ask.

needs_config_state

Re-reads a document when its sphinx-needs TOML file or the needs it imports change.

Sphinx extension: re-read a document when its needs vocabulary or imports change.

A document’s need types, links and fields come from a TOML file (needs_from_toml, set by zdocs_conf from ZDOCS_NEEDS_CONFIG). Sphinx re-reads every document when a config value registered with rebuild "env" changes, but sphinx-needs registers needs_types, needs_links and needs_fields with rebuild "html", and the config value needs_from_toml is only the file’s path. So after the file gains a link type, an incremental build reuses the pickled needs, which have no entry for the new link, and the first new need that uses it crashes the build: KeyError: "Link type 'fulfills' does not exist in backlinks.". A clean build works.

This extension registers zdocs_needs_config_digest, rebuild "env", and sets it to the SHA-256 of the file’s content. A change to the file changes the value, and Sphinx re-reads the document (“config changed”). The value is the same for both build stages, so the stage caches stay valid while the file does not change.

Re-reading is not enough when a link type is REMOVED: the pickled environment still holds needs imported from a peer’s needs.json under the old vocabulary, and the importing document crashes the same way once, although the peer’s file no longer has the link. So on a change the pickled environment is also dropped before Sphinx loads it (config-inited runs first), and the document starts from a fresh one, as in a clean build. The digests the environment was built with are kept beside it, in the doctree directory.

The needs that a document imports (needs_external_needs) have a similar problem. sphinx-needs loads them again on each build, but Sphinx writes only the pages of the documents that it read. When a peer’s needs.json gains a need that links to a need of this document, the source of the page with that need does not change. So Sphinx does not write the page again, and the page does not show the new incoming link. For example, a requirement page did not show “assessed by” after the test report gained adequacy needs.

So the extension also registers zdocs_imported_needs_digest, rebuild "env": a SHA-256 over the needs that each imported file gives. A change to it also starts from a fresh environment. This also removes the needs that a peer no longer has.

needs_config_state.STAMP_NAME = 'zdocs-needs-config.sha256'

the digests it was built with.

Type:

Beside the pickled environment

needs_config_state.ENV_PICKLE = 'environment.pickle'

Sphinx’s pickled environment in the doctree directory.

needs_config_state.needs_config_digest(confdir, from_toml)[source]

SHA-256 of the needs TOML file, or "" if none is set or it is unreadable.

from_toml is resolved against confdir, as sphinx-needs resolves it.

needs_config_state.imported_needs_digest(confdir, external_needs)[source]

SHA-256 over the needs that needs_external_needs imports, or "" if none.

Only json_path sources count. A relative path is resolved against confdir, as sphinx-needs resolves it. A json_url source is not read.

needs_config_state.drop_stale_environment(doctreedir, digest)[source]

Delete the pickled environment in doctreedir unless it was built with digest.

An environment with no recorded digest (a build dir from before this extension) counts as stale. Returns whether one was deleted; records digest either way.

needs_fields

Which optional need fields a consumer declared.

Optional need fields: set one only where the consumer declared it.

A need type, a link and a field are the consumer’s vocabulary (needs_config.toml). The engine can fill some fields the consumer may not want; setting an undeclared one makes sphinx-needs warn “Unknown option” on every need. So an optional field is set only when the running sphinx-needs schema has it, and how it is declared decides whether its value survives.

needs_fields.DEPENDS_ON = 'depends_on'

The Kconfig conditions of a test case or API symbol (@kconfig_depends{<condition>}), joined with "; ".

needs_fields.field_type(env, name)[source]

The declared schema type of need field name, or None if undeclared.

needs_fields.depends_field(env, conditions, subject)[source]

Whether to set depends_on for subject’s conditions.

Declare it as a string field: the value is the conditions joined with "; ", verbatim. An array field works too, as long as no condition contains one of ; | , — sphinx-needs would split A || B or IS_ENABLED(A, B) into pieces — so such a need gets no field and a warning instead of a silently wrong value.

xref_builder

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

Stage-1 xref builder for the two-stage documentation build.

Cross-referenced documents form a dependency cycle: each doc’s HTML needs the indexes (objects.inv / needs.json / Doxygen tag files) of the docs it links to. Those indexes depend only on a doc’s own content, so the build is split in two:

  • stage 1 — every doc emits just its index (this builder), no HTML;

  • stage 2 — every doc builds HTML with all indexes present, so cross references resolve.

This builder subclasses Sphinx’s dummy builder: it reads/parses the whole project (populating the environment) but writes no HTML. In finish() it dumps objects.inv for intersphinx. needs.json is handled separately by sphinx-needs itself: when needs_build_json = True its build-finished hook writes needs.json into the same output directory regardless of the active builder — so a sphinx-needs doc gets both artifacts from one xref build.

Stage 2 uses a SEPARATE doctree cache (-d) and parses for itself, once every peer index exists. Sharing one cache is what this build system used to do, and it was wrong: a role that resolves at parse time — :external+<inv>: is one — was resolved during stage 1 against inventories that did not exist yet, and stage 2, reusing that parse, wrote the empty result out. See the doctree comment in zdocs/cmake/sphinx.cmake.

class xref_builder.XrefBuilder(app: Sphinx, env: BuildEnvironment)[source]
name = 'xref'

The builder’s name. This is the value used to select builders on the command line.

epilog = 'Cross-reference index written to %(outdir)s.'

The message emitted upon successful build completion. This can be a printf-style template string with the following keys: outdir, project

get_target_uri(docname, typ=None)[source]

Return the target URI for a document name.

typ can be used to qualify the link characteristic for individual builders.

finish()[source]

Finish the building process.

The default implementation does nothing.

doxygen_parser

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

Doxygen XML parsing — no Sphinx dependency.

class doxygen_parser.MemberInfo[source]

Where a <ref> in Doxygen prose points, as HTML directory URLs.

A symbol Doxygen resolved through a tag file carries the tag file’s path in external=; tags maps such a path to the HTML directory of the document that tag file belongs to, so each reference goes to the project that documents the symbol. api is for a tag-file reference tags does not name — the API document the test specification references. local is for a symbol documented in the parsed project itself, such as a shared test procedure. An empty string leaves that kind unlinked.

api: str

Alias for field number 0

local: str

Alias for field number 1

tags: Mapping[str, str] | None

Alias for field number 2

doxygen_parser.ref_to_rst(ref: Element, links: RefLinks | None) → str | None[source]

A <ref> as an RST hyperlink into the Doxygen HTML, or None if it has none.

Doxygen’s refid is <compound>_1<anchor> for a member and the bare compound for a page or group. The caller decides what an unlinked ref becomes.

doxygen_parser.elem_text(elem: Element | None) → str[source]

Walk the element tree collecting all text nodes (.text and .tail), join them, then normalise any runs of whitespace down to single spaces.

doxygen_parser.para_text(para: Element | None, links: RefLinks | None = None) → str[source]

Extract inline text from a <para>, rendering code/ref as plain text.

With links, a symbol reference becomes a hyperlink into the Doxygen HTML (see ref_to_rst) — the same target the see-also line links to. Without, a member reference becomes a :c:func: role, which resolves only where a C domain defines the symbol; in the test documents nothing does, so it renders as unlinked code, silently. The need title must stay plain text (it is not parsed), so a caller building one passes no links.

doxygen_parser.list_to_rst_lines(listelem: Element, marker: str, links: RefLinks | None = None) → list[str][source]

Convert <orderedlist> or <itemizedlist> children to RST list lines.

Each item’s paragraphs go through para_rst_lines, so a list nested inside a list item survives — mutual recursion between the two functions, matching doxygen’s own nesting (<listitem><para>text<itemizedlist>…). A blank line is what makes an indented block a nested list rather than a continuation of the item’s text, which is why the blank lines para_rst_lines returns are load-bearing and must be indented as blanks (i.e. emitted empty), never dropped.

doxygen_parser.para_rst_lines(para: Element | None, links: RefLinks | None = None) → list[str][source]

One <para> as RST lines: its own prose first, then any lists it contains.

para_text deliberately skips orderedlist/itemizedlist (and parameterlist/simplesect/xrefsect, which callers render separately), so it answers “what does this paragraph SAY” and cannot answer “what does it CONTAIN”. Reading the lists here is what stops an authored list from being dropped: doxygen puts Test steps: and its bullets in two sibling <para> elements, and rendering only the first published the label with nothing under it — a clean exit with the content silently absent.

Handles text and a list in the SAME <para> (prose, blank line, list) as well as the sibling-<para> shape, because both occur and only one of them was ever exercised. Returns no trailing blank line; joining blocks is the caller’s job.

doxygen_parser.section_to_rst(simplesect: Element, links: RefLinks | None = None) → list[str][source]

Render a <simplesect kind=”par”> (Arrange/Act/Assert) into RST lines. Emits a .. rubric:: for the title, then renders each <para> child as an ordered list, unordered list, or plain prose paragraph.

doxygen_parser.see_to_rst(simplesect_see: Element | Iterable[Element], api_html_dir: str, testspec_html_dir: str = '', tag_dirs: Mapping[str, str] | None = None) → str[source]

Render one <simplesect kind=”see”>, or all of a member, into a ‘See also:’ RST line. Each <ref> becomes a hyperlink (see ref_to_rst), a :c:func: role (unlinked member refs), or a plain code span, depending on its attributes. Text that is not in a <ref> becomes a literal: @see irq_offload() gives no <ref> when no Doxygen project documents the symbol, and it was lost. Doxygen 1.16 writes one see section for each @see line, also for lines that follow each other, so a caller gives all sections of a member (findall), not only the first. tag_dirs: RefLinks.tags.

doxygen_parser.extract_params(detaileddesc: Element, links: RefLinks | None = None) → list[tuple[str, str]][source]

Walk all <parameterlist kind=”param”> elements in the detailed description, collect each parameter’s name(s) and prose description, and return them as (name, description) pairs. Multiple names per item are joined with ‘, ‘.

doxygen_parser.detail_rst_lines(dd: Element | None, links: RefLinks | None = None) → list[str][source]

A <detaileddescription>’s own prose as RST LINES — paragraphs and lists.

Shared by member-level (@details on a ZTEST/function) and compound-level (@details on a @defgroup) descriptions — both are the same <detaileddescription><para>…</para></detaileddescription> shape.

Returns LINES, not one string per paragraph, which is the whole point: a bullet list cannot be represented as a single line. The previous version collapsed each <para> with para_text and dropped the ones that came back empty, so an authored

Test steps:

  • Return success

published the label and nothing else, because doxygen emits the label and the list as two sibling <para> elements and only the first survives a text-only read. Every caller therefore indents PER LINE and preserves blank lines, exactly as it already did for body_sections.

Structural children stay excluded (parameterlist, simplesect, xrefsect): callers render params, see-also and the test-id/requirement xrefsects separately, and rendering them here would duplicate them.

doxygen_parser.parse_memberdef(memberdef: Element, compound_id: str, testspec_html_dir: str, api_html_dir: str, tag_dirs: Mapping[str, str] | None = None) → MemberInfo[source]

Extract all structured fields from a <memberdef> element — name, source location, Doxygen URL, brief description, test ID, requirement refs, status, see-also, and Arrange/Act/Assert body sections — and return them as a MemberInfo.

tag_dirs: where a tag-file reference links, by tag file (RefLinks.tags).

doxygen_parser.requirement_uids(memberdef: Element, relation: str) → list[str][source]

The requirement UIDs of Doxygen’s native \verifies or \satisfies.

relation is the element name, verifies or satisfies: a child of the memberdef itself, not of its description, with the UID only in each <requirement>’s refid, in source order, each UID once.

The refid is NOT proof the requirement exists: Doxygen synthesizes requirement_<UID> from the UID string whether or not any \requirement defines it, so a typo is byte-identical here to a real link. Doxygen’s “Reference to unknown requirement” warning tells them apart, and so does the link target’s absence among the needs, which sphinx-needs reports for the link these UIDs become.

doxygen_parser.kconfig_depends(dd: Element | None) → tuple[str, list[str]][source]

(label, conditions) of the kconfig_depends xrefitems in dd.

@kconfig_depends{<condition>} is an alias for \xrefitem kconfig_depends "Depends on" "Kconfig dependencies" <condition>. Doxygen 1.16 merges adjacent commands into ONE xrefsect with a <para> per condition, while commands elsewhere in the comment get xrefsects of their own; like @testid, they can sit in the last paragraph or in the last list item, so the whole description is searched. Each condition is kept verbatim ((CONFIG_A && !CONFIG_B) || CONFIG_C), once, in source order, without the trailing space Doxygen adds. label is the xrefitem’s title as the consumer’s alias spells it (“Depends on”), or "" if there is none.

class doxygen_parser.SymbolInfo[source]
doxygen_parser.parse_symbol(memberdef: Element, html_dir: str) → SymbolInfo[source]

An API symbol (function, macro, …) as a need’s data: name, kind, the requirements it satisfies, its brief, where it is declared, and its page in the Doxygen HTML under html_dir.

The page is the one Doxygen documents the member on, which its id names: <compound>_1<anchor>, the same shape ref_to_rst links. A member in a group is documented on the group’s page, not the header’s.

doxygen_parser.load_group_index(xml_dir: Path) → dict[str, str][source]

Parse Doxygen’s index.xml and return a dict mapping each group’s name to its refid, which is used as the filename stem for the group’s XML file.

rst_builders

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

RST string builders — no Sphinx dependency.

rst_builders.slugify(s)[source]

Replace non-alphanumeric runs with ‘-’ and strip leading/trailing dashes.

rst_builders.build_need_rst(info, suite_name, module_path='', suite_title='', need_names=None, depends_field=False, id_scope=None)[source]

Build the RST block for a single test_case need.

depends_field: set the depends_on field (see _depends_on_rst). id_scope: what the fallback id testspec-<scope>-<function> is scoped by — the suite’s Doxygen group name, which differs from suite_name under a testmodule_suite_qualifier; defaults to suite_name.

rst_builders.build_procedure_need_rst(memberdef, proc_compound_id, proc_group_name, testspec_html_dir, api_html_dir, need_names=None, tag_dirs=None)[source]

Build a test_procedure needs item for one shared test procedure.

tag_dirs: where a tag-file reference links (doxygen_parser.RefLinks.tags).

rst_builders.build_result_rst(r, spec_id, test_module, req_ids=None, need_names=None, fields=())[source]

Build RST block for one test_result need.

fields: the result-field roles (RESULT_FIELD_ROLES) to set, under their names from need_names, where r has a value for them — the ones the consumer declared; an undeclared option warns per need.

rst_builders.build_symbol_need_rst(info, need_names=None, depends_field=False)[source]

Build the RST block for one API symbol’s need (the implementation role).

info is a doxygen_parser.parse_symbol result. The requirements it satisfies become the satisfies link, so each requirement’s page lists the symbol under the link’s incoming name, beside its verifying test cases. The kind, declaration and Doxygen page go in the body, where no field has to be declared for them. depends_field: set the depends_on field (see _depends_on_rst).

rst_builders.build_scenario_table(testcase_yaml_path)[source]

Return RST lines for a list-table of scenarios from a scenario file.

rst_builders.find_scenario_yaml(module_dir)[source]

Return the first scenario file in module_dir that exists, or None.

The names come from SCENARIO_YAML_NAMES. When no file exists, log a warning that names each file it tried.

twister_reader

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

Twister output parsing — no Sphinx dependency.

twister_reader.split_case_name(name, scenario)[source]

(suite, function, instance) of twister test case name in scenario.

Twister names a case <scenario>.<suite>.<fn>, with ztest’s test_ stripped from fn (and so from function here). A parameterized test (ZTEST_P) reports one case per value as <scenario>.<fn>[<instantiation>/ <value>]: no suite segment (suite is ""), and the value may contain anything, dots included, so it is split off before the name is. instance is the part in brackets, or None.

twister_reader.normalise_test_path(path)[source]

A testsuite path in one spelling: forward slashes, no ./, no trailing /.

Twister writes path relative to ZEPHYR_BASE with forward slashes; a consumer typing the same directory may add a trailing slash or a leading ./, or come from a Windows checkout. None of that changes which directory it is.

twister_reader.scenario_selected(platform, scenario, module_filter=None, exact=False, path_filter=None, suite_paths=None)[source]

Whether a (platform, scenario) run belongs to the report being built.

module_filter matches the scenario name, as a dotted prefix (or exactly with exact). path_filter matches the testsuite directory exactly, looked up in suite_paths (see testsuite_paths). With both, a run must satisfy both. With neither, every run is selected.

The scenario prefix alone is not a module: upstream scenario names do not follow the directory layout (kernel.timer is tests/kernel/timer/timer_api, and prefixes kernel.timer.error_case from timer_error_case), so only the path identifies a module’s results reliably.

twister_reader.testsuite_paths(twister_meta)[source]

{(platform, scenario): normalised path} from a loaded twister.json.

twister_report.xml carries no path, only the scenario (classname) per platform, so the path a result came from is found here, by the same pair.

twister_reader.testcase_statuses(twister_meta)[source]

{(platform, testcase identifier): status} from a loaded twister.json.

twister.json keeps statuses the JUnit XML cannot express — blocked in particular, which the XML reports as a failure.

twister_reader.fold_parameterized_results(results, spec_lookup=None, twister_statuses=None)[source]

Attach each parameterized test’s value results to its aggregate result.

Twister reports a ZTEST_P function once as the aggregate <scenario>.<suite>.<fn> (from ztest’s summary) and once per value as <scenario>.<fn>[<instantiation>/<value>] (see parse_twister_results, which marks the latter with instance). The spec has one test case for the function, so the values belong to the aggregate of the same run (platform and scenario), found by the function name.

A run without an aggregate gets one, with the suite taken from the aggregates of other runs of the same scenario and function if they name exactly one, else from the spec (spec_lookup) if exactly one case carries the function.

Returns (results, unmatched): the results without the value entries, and the function names whose values could not be attached, once each.

class twister_reader.SpecLookup(entries=())[source]

The spec’s test cases, found by the (suite, function) of a twister result.

ZTEST function names are not unique across suites (some 300 are reused in the Zephyr test tree), so the suite is part of the key. Each entry is a dict with at least id, suite and test_function.

A result’s function has ztest’s test_ prefix stripped, while the spec records the C name as written, so both spellings are tried. When the result’s suite has no such case, the bare function name is used instead — but only if exactly one case carries it. candidates() returns every match, so an ambiguous one (several suites, or one (suite, function) pair documented in several test modules) is visible to the caller; find() returns a case only when there is exactly one.

twister_reader.load_spec_lookup(json_path, need_names=None)[source]

Read spec needs.json; return a SpecLookup of its test cases.

need_names is the same role->name mapping rst_builders.py emitters take (testmodule_need_types/testmodule_need_links, merged by the caller) — the “case” role’s need type and the “verifies” role’s link name are both consumer-configurable (step 26), and this lookup must filter/read by whatever names the consumer’s spec needs actually carry, not the engine’s own defaults. Passing nothing preserves the original literal behaviour, which is what the unchanged unit tests pin.

twister_reader.find_handler_log(twister_out_dir, platform, toolchain, test_path, scenario_name)[source]

Return the Path to handler.log for a (platform, scenario) run, or None.

twister_reader.find_build_config(twister_out_dir, platform, toolchain, test_path, scenario_name)[source]

Return the Path to the Kconfig .config of a (platform, scenario) build, or None.

Twister keeps each build under the run directory find_handler_log describes, with the build’s zephyr/.config in it. Unlike the log, the configuration is looked up only where twister puts it (the path layout, or the flat --detailed-test-id one): a .config of another scenario would answer for a build it did not come from.

twister_reader.read_kconfig(path)[source]

The Kconfig symbols a .config sets, as a set of names.

A symbol is set when it has a value (=y, =m, a number, a string); # CONFIG_X is not set and a symbol that is absent are not.

exception twister_reader.UnparseableCondition[source]

A depends_on condition outside the grammar evaluate_condition reads.

twister_reader.evaluate_condition(condition, symbols)[source]

Whether Kconfig condition holds for the set symbols (read_kconfig).

The grammar: CONFIG_X and defined(CONFIG_X) (or defined CONFIG_X) are true iff the symbol is set; !, &&, || (in C’s precedence) and parentheses combine them. Anything else — another macro, IS_ENABLED(), a comparison, a number — raises UnparseableCondition: its value in the build is not known from .config alone.

twister_reader.depends_met(conditions, symbols)[source]

(value, unparseable) for a test case’s depends_on in one build.

conditions are the case’s conditions, all of which must hold (the "; " of depends_on is an and); symbols is the build’s set Kconfig symbols, or None when its .config was not found. The value is "yes" or "no", or "n/a" when the case has no condition, the build has no .config, or a condition is outside evaluate_condition’s grammar — then unparseable lists those conditions, and no value is guessed, even when another condition is false.

twister_reader.skip_class(result, met)[source]

The class of a skipped result (None for any other), given its depends_met.

build-only: twister built the test but did not run it. platform: the platform could not take it (a memory region overflowed, or it was filtered by platform). config: ztest skipped it and the case’s depends_on is false in the build. unexplained: anything else, including a ztest skip whose condition holds, cannot be evaluated, or is not recorded.

twister_reader.load_twister_meta(json_path)[source]

Load and validate twister.json; return the dict.

adequacy

Coverage adequacy, with no Sphinx dependency of its own.

Coverage adequacy: do the own tests of a requirement run the code that satisfies it.

This module has no Sphinx dependency. A verifies link and a satisfies link are claims. A per-test coverage run (west twister --coverage-per-test) checks them against execution. For each requirement, the module finds the bodies of the satisfying symbols in the sources of the run commit. Then it compares their lines with the lines that the own verifying tests covered.

The port starts from doc/_scripts/traceability_app.py by Anas (zephyr collab-safety 0cc56a35003). Source, resolve_impl_symbols and the verdicts (evidence, adequacy) are close to the original. The keys are the keys of zdocs:

  • A requirement gets its verifying test cases through the verifies link of the case needs.

  • A requirement gets its satisfying symbols through the satisfies link of the implementation needs (<TYPE>-<symbol>, rst_builders.symbol_need_id).

  • A test case gets its matrix keys through the twister.json of the run. Each twister case goes to its spec case by (suite, function), as a test result does. The key is built from (scenario, C function name), as twister writes it. The module does not parse keys, because scenario names are prefixes of other scenario names (kernel.lifo, kernel.lifo.usage).

Two changes from the original correct errors:

  • File patterns use glob rules in both modes: **/ is zero or more directories, and * stays in one directory. The original used fnmatch on the git ls-tree output. There, **/ needs at least one directory, so include/zephyr/sys/**/*.h did not find sys/slist.h.

  • The run commit comes from zephyr.sha beside the twister output first. Then it comes from environment.zephyr_version.

One change from the original is a setting: the files that hold the bodies. IMPL_PATTERNS is the set of the original and the default. A consumer gives its own set to assess (testcoverage_impl_files in Sphinx).

The verdicts are the verdicts of the original:

true

Every symbol that coverage can judge has a body that the own tests ran.

partial

Some of these symbols have such a body, others do not.

broken

Other tests of the run reach the code. The own tests never do.

unattributed

No test of the run covers any body, so coverage cannot judge the link.

unresolved

Satisfying symbols exist, but none maps to a body (a macro).

no-impl

No symbol satisfies the requirement.

no-cov

The verifying tests have no coverage data in this run.

adequacy.VERDICTS = ('broken', 'partial', 'unattributed', 'unresolved', 'no-cov', 'no-impl', 'true')

the findings first.

Type:

In the order a report lists them

adequacy.matrix_key(scenario, function, suite=None)[source]

The test_matrix.json key of C function function in scenario.

Twister names a per-test tracefile <scenario>.<test>. If two suites of one scenario have the same test name, it uses <scenario>.<suite>.<test>. The key is that name, with _ for each character outside [A-Za-z0-9_]. Give suite for the second form. function keeps its test_ prefix.

adequacy.IMPL_PATTERNS = ('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h')

Where the original looks for bodies. The default of impl_files.

adequacy.keep_prefixes(impl_files=('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h'))[source]

_KEEP and the fixed start of each pattern, up to its last / before a wildcard.

A body file must be in the matrix that load_matrix keeps, so that its lines can count as covered. arch/**/*.c adds arch/. A pattern with a wildcard in its first part adds nothing, so that the matrix does not keep files outside the tree (../modules/...).

adequacy.load_matrix(matrix_path, keep=('kernel/', 'include/', 'lib/', 'tests/'))[source]

(by_test, by_line) of a test_matrix.json, for the files under keep.

  • by_test[key] = {file: set of covered lines}

  • by_line[file] = {line (int): [keys]}

keep: path prefixes (_KEEP, or keep_prefixes of the body files).

adequacy.run_commit(run_dir, environment, root)[source]

The commit that the run built, as a full sha in root, or None.

The first source is zephyr.sha beside the twister output. The second source is the -g<hash> of environment.zephyr_version in twister.json.

adequacy.run_name(run_dir, sha, root)[source]

The name of the run: the first tag (sorted) on its commit.

If the commit has no tag, the name is the name of the run directory. Each run of characters outside [A-Za-z0-9_-] becomes one -.

class adequacy.Source(root, ref=None)[source]

Reads the files of the tree at the commit of the coverage run.

Coverage line numbers are correct only for the sources of the build that made them. With ref (the run commit, run_commit), read uses git show <ref>:<path>. The line ranges are then correct also after the working tree changes. Without ref, read uses the working tree under root, and the caller warns.

list(patterns)[source]

The relative paths that match one of the glob patterns.

read(rel)[source]

The text of the file at the ref, or None.

adequacy.resolve_impl_symbols(source, symbols, impl_files=('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h'))[source]

The function bodies of the satisfying symbols (best effort).

A system call has more than one body. z_impl_<sym> is the implementation in supervisor mode. z_vrfy_<sym> is the verifier that a ZTEST_USER test reaches instead. The verifier can do the operation itself and never call the z_impl body (z_vrfy_k_thread_create does this). The function collects all bodies. A test that runs one of them runs the implementation.

A definition starts at column 0 (static [ALWAYS_INLINE] inline is permitted), and its line does not end in ;. Its body ends at the first } in column 0, in 500 lines or less. In a header, only a static line counts: other lines are prototypes or macros. So a macro has no body. A header is a .h file anywhere in the tree (kernel/include/ too).

impl_files: the glob patterns of the files to search (IMPL_PATTERNS).

Returns {sym: [{“file”, “a”, “b”, “variant”}, …]}.

class adequacy.CoverageRun(name, sha, environment, by_test, by_line, cases, unmatched)[source]

One per-test coverage run, joined to the test cases of the spec.

  • cases[case id] = {"statuses": [...], "keys": [matrix keys]}, for each spec case that twister ran in this run.

  • case_of_key[key] = {case ids}: the reverse map, for the question “which tests ran this line”.

  • unmatched: the twister cases with no unique spec case (sorted names).

adequacy.load_coverage_run(run_dir, spec_lookup, root, name=None, impl_files=('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h'))[source]

Read a coverage run directory: twister.json, coverage/test_matrix.json, zephyr.sha.

impl_files: the body files of resolve_impl_symbols. The matrix keeps their lines (keep_prefixes).

Returns (run, inputs): the CoverageRun and the files that it reads.

(spec_lookup, verified_by, satisfied_by, ids) from needs.json files.

  • spec_lookup: a SpecLookup of the test cases (type role case).

  • verified_by[req] = [case ids], through the verifies role.

  • satisfied_by[req] = [symbols]: the implementation needs (type role implementation, ids <TYPE>-<symbol>), through satisfies.

  • ids: each need id in the files. A link target that is not a need gets no assessment.

If several files export one need, it counts once.

adequacy.evidence(case_ids, run)[source]

The state of the verifying cases: untested, no-run, failing, passing or skipped.

adequacy.adequacy(symbols, case_ids, run, impl_loc)[source]

{"verdict", "impls"} of a requirement from its symbols and case_ids.

impls has one entry for each symbol and body, with these keys:

  • sym and variant.

  • file, a and b: None for a symbol with no body.

  • own: the number of body lines that the own tests ran.

  • any: the number of body lines that any test of the run ran.

  • own_tests: {case id: [lines]}.

  • other_tests: {matrix key: [lines]}, for the keys of other tests.

adequacy.assess(run, verified_by, satisfied_by, source, ids=None, impl_files=('kernel/*.c', 'kernel/**/*.c', 'include/zephyr/kernel.h', 'include/zephyr/kernel/**/*.h', 'include/zephyr/sys/**/*.h'))[source]

The assessment of each requirement in the scope of the run: ({req: result}, impl_loc).

A requirement is in the scope if twister ran at least one of its verifying cases in this run. If ids is given, the requirement must also be a need. Each result has verdict and impls (adequacy), evidence, symbols and cases. impl_files: the body files (resolve_impl_symbols).

adequacy.line_ranges(lines)[source]

"79-81, 84" for sorted line numbers.