Directives and roles
What a document author writes in RST. Every directive here is loaded for
every document by zdocs_conf.configure(), whether
or not the document uses it — an extension list that varied by document would
make the two build stages’ configuration differ and invalidate the shared
doctree cache. The testmodule/testreport/twisterinfo directives and
symbolneeds are the exceptions: they load only for a document whose
registry entry carries a testmodule: or symbol_needs: block
(The registry schema).
.. doc_control::
The controlled-document header table: owner, classification, approval dates,
version, supersession. Source:
doc_control.DocCtrlDirective.
.. doc_control::
:owner: Quality Team
:classification: SOP
:author: Jane Doe <jane@example.com>
:approved_by: John Roe <john@example.com>
:approval_date: 2026-01-15
owner is the only required option. Everything else defaults to a
placeholder string ("not-authored-yet", and similarly for
reviewed_by/approved_by) so the table always renders a complete row set
even for a document nobody has touched yet — these placeholders are computed
at build time, never written back into the source; docctl (Command-line tools) is
what edits the source.
version defaults to the document’s resolved git-tag version (the same
value shown in the sidebar) rather than requiring it to be typed twice;
:version: overrides it. classification is checked against
doc_control_classifications (a conf.py config value, default a list of
eight QMS-flavoured terms — "SOP", "Record", "Policy", and so on);
set it to your own vocabulary in conf.py, or to an empty list to accept
anything.
Two further conf.py values control PDF-only behaviour: signature_section
("none" default, or "top"/"bottom" to insert a sign-off block with
ruled lines for Author/Reviewer/Approver, pre-filled from this directive’s own
fields where set) and releaselevel (default "next", read by
sphinx.ext.ifconfig for .. ifconfig:: releaselevel not in (...) blocks).
Neither has any effect in HTML.
:qmsdoc: role
References a kind: external/sphinx-external/doxygen-external
registry document by id — the only way to link to one, since it has no local
Sphinx label and (for plain external) is deliberately excluded from
intersphinx (there is no objects.inv to fetch). Source:
qms_ref.QmsDocRole.
See :qmsdoc:`sop-swdp` for the full procedure.
See :qmsdoc:`SOP-SWDP <sop-swdp>` for the full procedure.
Renders as a real hyperlink for HTML, and as plain text for every other
builder (a PDF has no notion of a live web link). Referencing an id that is
not a documents.yaml entry of one of those three kinds is a build error at
the point of use.
.. latexinclude::
Includes another RST file, for the latex builder only — a no-op everywhere
else. For content that has to travel with a PDF because a printed document
carries no hyperlinks: a shared glossary, a terms appendix. Source:
latexinclude.LatexIncludeDirective.
.. latexinclude:: ../_glossary_terms.rst
The path is resolved relative to the file as you wrote it, not to the
generated build tree external_content copies sources into — the directive
reconstructs your authored directory from confdir and docname
specifically so that .. means what it looks like it means. A missing file
is a build error naming the resolved path, not a silently empty include.
.. testmodule::, .. testreport::, .. twisterinfo::
The chain from annotated ztest C source to a rendered, traceable test report
— see From annotated C to a test report for the mechanism and
Rendering test specifications from annotated source for a worked recipe. Source:
test_module.
.. testmodule:: widget_probe_module
:module: checks/widget/probe
testmodule’s argument is a Doxygen @defgroup name (the module group,
never a suite or a path); :module: is a project-relative path used only to
locate that module’s scenario file for the rendered scenario table: the first
of testcase.yaml, tests.yaml and sample.yaml that exists. Every
ZTEST/ZTEST_SUITE/… in the named group and its inner suite/procedure
groups becomes one need each — nothing is written by hand per test case.
A test case need’s suite field is its inner suite group’s name, so name
that group after the ZTEST_SUITE: testreport finds the test case for a
twister result by the pair (suite, function). Two test modules that declare the
same ztest suite (Zephyr’s workq user_work and work_queue both declare
workqueue_api) cannot share one group, though, because then both module
pages render every test in it and the need ids collide. So give each module its
own group, and set a qualifier in the conf.py of the document that holds
the testmodule directives, after the configure() call:
testmodule_suite_qualifier = "__"
/** @defgroup kernel_workq_user_work_module__workqueue_api workqueue_api ZTest suite
* @ingroup kernel_workq_user_work_module */
The suite is the part of the group name after the qualifier’s last
occurrence (here workqueue_api). A group name without the qualifier is used
whole, and so is every name when the value is unset (the default, ""). The
qualifier splits only the Doxygen group name the suite is derived from. The
(suite, function) key a result is correlated by does not change, and it now
sees the real suite name, which is the point. The fallback id of a test case
without @testid, testspec-<group>-<function>, keeps the whole group
name: two modules may have a function of the same name in the same suite, and
their ids must not collide. A result of such a function is still ambiguous by
(suite, function), and testreport skips it with a warning, as it does for
any pair that two modules document.
.. testreport:: twister_report.xml
:path: tests/kernel/timer/timer_error_case
.. twisterinfo:: twister.json
testreport’s and twisterinfo’s arguments are filenames resolved
against ZDOCS_TWISTER_OUT (or the including document’s own directory, as a
fallback, if that is unset) unless given as an absolute path.
testreport selects which runs a page shows with two optional options:
:path:A test directory, exactly as twister records it in
twister.json’s testsuitepath— relative toZEPHYR_BASE, e.g.tests/kernel/timer/timer_error_case(a test root outside the Zephyr tree reads../<project>/tests/...). Compared exactly after normalising slashes, a leading./and a trailing/; never as a prefix. The path comes from thetwister.jsonbeside the report XML (the XML has none), and each result is matched to its testsuite by platform and scenario name. If thattwister.jsonis missing, the directive soft-fails to a “not found” paragraph like the other inputs.:module:A scenario-name prefix, matched against the JUnit
classname(the scenario itself, or<module>.followed by anything).
With both, a run must match both. With neither, the page shows every result in
the report. :path: is the one that identifies a test module: scenario
names do not follow directories upstream — tests/kernel/timer/timer_api runs
as kernel.timer, a prefix of timer_error_case’s kernel.timer.error_case
— so :module: alone can put one module’s results on another’s page, where
the second page then fails with “A need with ID … already exists”. The
execution-log section and the result summary follow the same selection, so a
page is consistent with itself.
A parameterized test (ZTEST_P) gets one result need per run, not one per
parameter value: the values’ results are attached to the test’s aggregate
result, which takes its status from them and lists the values that did not
pass (From annotated C to a test report). No need type or field is
added for this; the values render in the need’s body.
Each result also says whether its build met the test case’s depends_on
(the @kconfig_depends conditions below), and each skipped result why it was
skipped. Both are read from the run, not from the spec alone:
depends_metyesorno: the case’s conditions, all of which must hold, evaluated against the.configtwister kept for that build (<platform>/<toolchain>/<test path>/<scenario>/zephyr/.configunder the report’s directory).CONFIG_Xanddefined(CONFIG_X)are true when the symbol has a value (=y, a number, a string; notis not set);!,&&,||and parentheses combine them.n/awhen the case has no condition, the build’s.configis not there, or a condition uses anything else (another macro,IS_ENABLED(), a comparison): its value is not known from.config, so none is guessed, and the build warns once per case and condition. A result that passed withnoran although its condition was false.skip_classOn skipped results only.
build-only: twister built the test but did not run it.platform: a memory region overflowed, or the platform was filtered out.config: ztest skipped it anddepends_metisno.unexplained: anything else.
Both are set only where your needs_config.toml declares them (string
fields), under names you may choose, like the need types and links:
testreport_need_fields = {"depends_met": "depends_met", "skip_class": "skip_class"} # defaults
Both directives soft-fail to a short “not found” paragraph
when their input is absent, rather than failing the build — a documentation
build outrunning its test run is a normal pipeline state. testmodule does
not soft-fail on a missing Doxygen group: annotated source is expected to
always be present, so a miss there is treated as a real error.
Two conf.py values let a project rename the three need types
(case/procedure/result) and three link types
(verifies/result_of/covers) the directives emit — the engine
thinks in roles, never in literal names:
testmodule_need_types = {"case": "probe", "procedure": "routine", "result": "outcome"}
testmodule_need_links = {"verifies": "confirms", "result_of": "produced_by", "covers": "spans"}
Omit them entirely and you get the engine’s own defaults
(test_case/test_procedure/test_result,
verifies/result_of/covers). Whatever names you choose, every one —
plus nine custom fields the directives attach to needs
(test_function, test_module, suite, suite_title,
platform, scenario, twister_id, execution_time, reason) —
must be declared in your needs_config.toml
(ZDOCS_NEEDS_CONFIG, The ZDOCS_* contract), or sphinx-needs rejects the
need with an Unknown option/Unknown need type warning per occurrence.
Doxygen annotations feeding these directives use two custom Doxygen
ALIASES your Doxyfile.in declares yourself (the alias names are
yours; the \xrefitem keys testids/reqrefs they expand to are what
the parser matches on, and must be spelled exactly):
ALIASES += "testid{1}=\xrefitem testids \"Test ID\" \"Test IDs\" \1"
ALIASES += "reqref{1}=\xrefitem reqrefs \"Requirement\" \"Requirements\" \1"
A third alias is optional. @kconfig_depends{<condition>} records the
Kconfig condition a test case (or, with symbolneeds, an API symbol) is
built under; the key kconfig_depends is what the parser matches, the titles
are yours and the first one labels the rendered line:
ALIASES += "kconfig_depends{1}=\xrefitem kconfig_depends \"Depends on\" \"Kconfig dependencies\" \1"
Every condition is kept verbatim ((CONFIG_A && !CONFIG_B) || CONFIG_C;
write a comma as \,), once, in source order. It renders in the need’s body
(“Depends on: CONFIG_ASSERT”) on testmodule and symbolneeds needs,
and fills the optional field depends_on — the conditions joined with
"; " — if, and only if, your needs_config.toml declares it. Declare it
as a string field:
[needs.fields.depends_on]
description = "Kconfig conditions the need depends on"
nullable = true
[needs.fields.depends_on.schema]
type = "string"
Left undeclared, no need gets the field and nothing warns. Declared as an
array, it works only while no condition contains ;, | or ,,
where sphinx-needs splits an array value; a need with such a condition gets no
field and a warning instead.
/**
* @reqref{DUTY_001}
* @see acme_widget_init()
* @kconfig_depends{CONFIG_WIDGET_PROBE}
* @testid{WIDGET-PROBE-001}
*/
ZTEST(widget_probe_suite, test_widget_reports_initial_value)
{
...
}
.. testcoverage::
One adequacy need per requirement that a per-test coverage run can assess.
The verdict says if the requirement’s own verifying tests run the code that
satisfies it. test_module loads the directive.
.. testcoverage::
:run: nightly-cov
:layout: adequacy
The optional argument is the run directory. Without it, the directive reads
ZDOCS_COVERAGE_OUT (The ZDOCS_* contract). The run directory holds
twister.json, coverage/test_matrix.json and zephyr.sha. The
:run: option names the run in the need ids. Without it, the name is the
first tag (sorted) on the run commit. If the commit has no tag, the name is
the name of the run directory. The :layout: option sets the sphinx-needs
layout of each need.
The directive joins these inputs:
The requirement’s verifying test cases, through the
verifieslink of the case needs.The requirement’s satisfying symbols, through the
satisfieslink of the implementation needs (IMPL-<symbol>, seesymbolneeds).The test cases that the run ran. Each twister case goes to its spec case by (suite, function), as a test result does. Its matrix key is built from the scenario and the C function name (
kernel.lifo.usage+test_xgiveskernel_lifo_usage_test_x). The directive does not parse keys, because scenario names are prefixes of other scenario names.The needs come from
needs_external_needsandtestspec_needs_json.
The directive finds the bodies of each symbol in the sources of the run
commit (git show <sha>:<path> in testmodule_root). The commit comes
from zephyr.sha, else from the -g<hash> of
environment.zephyr_version in twister.json. If the commit is not in
the tree, the directive reads the working tree and warns. A body is
z_impl_<symbol>, z_vrfy_<symbol> (the verifier that a user-mode test
reaches), a plain definition, or a header static inline. A macro has no
body.
testcoverage_impl_files sets the files that hold the bodies. Each entry is
a glob pattern relative to testmodule_root: **/ is zero or more
directories, and * stays in one directory. In a .h file, only a
static inline definition is a body. The default is the set of the
original resolver:
testcoverage_impl_files = [ # default
"kernel/*.c",
"kernel/**/*.c",
"include/zephyr/kernel.h",
"include/zephyr/kernel/**/*.h",
"include/zephyr/sys/**/*.h",
]
An empty list gives the default. A symbol with its body outside these files
gets the verdict unresolved. The summary of the run lists the files that
the directive searched.
The verdicts:
trueThe own tests run every symbol that coverage can judge.
partialThe own tests run some of these symbols, not all.
brokenOther tests of the run reach the code. The own tests never do.
unattributedNo test of the run covers any body. Coverage cannot judge the link.
unresolvedNo satisfying symbol maps to a body (a macro).
no-covThe verifying tests have no coverage data in the run.
no-implNo symbol satisfies the requirement.
The directive assesses a requirement if the run ran at least one of its verifying test cases. It renders a summary of the run, a table of the verdicts, and one section per verdict. Each need lists its symbols and bodies. For each body, it gives the lines that each own test ran, and the other tests that ran the body.
The id of a need is ADQ-<run>/<requirement>. testcoverage_id_prefix
sets the prefix. The type, the link and the fields are roles, as for the other
directives:
testcoverage_need_types = {"adequacy": "adequacy"} # defaults
testcoverage_need_links = {"assesses": "assesses"}
testcoverage_need_fields = {
"verdict": "verdict", "evidence": "evidence", "coverage_run": "coverage_run",
"judged_symbols": "judged_symbols", "symbol_hits": "symbol_hits",
}
Declare the type and the link in your needs_config.toml. The directive
sets a field only if your needs_config.toml declares it (string fields).
The body of the need always shows the same information. The fields:
verdictOne of the verdicts above.
evidenceThe state of the verifying tests in the coverage run:
passing,failing,skipped,no-runoruntested.coverage_runThe name of the run.
judged_symbolsThe satisfying symbols, joined with
"; ".symbol_hitsPer symbol, the body lines that the own tests ran and that any test ran (
k_sem_init: own 11, any 11).
A parameterized test (ZTEST_P) has one matrix key for all its values: the
per-test dump of Zephyr has no value in its tag.
.. symbolneeds::
One need per API symbol (function, macro, …) carrying a Doxygen 1.16
\satisfies <UID>, linked to the requirement needs those UIDs name. Loads
only for a document whose registry entry has a symbol_needs: block
(The registry schema), which also names the Doxygen document whose XML is
read. Source: symbol_needs.
.. symbolneeds::
.. symbolneeds:: queue_apis
Without an argument, every annotated symbol in the Doxygen project is
emitted, in sections headed by the group (or file) that documents it. With a
Doxygen group name, only that group’s symbols, with no heading. Each symbol is
emitted once. A symbol without \satisfies gets no need.
Each need is titled with the symbol name and has the id
<TYPE>-<symbol>, where <TYPE> is the need type’s name in upper case
(IMPL-k_queue_init). Its body gives the kind, the brief, a link to the
symbol’s Doxygen page and the file it is declared in; no custom field is
needed for them. The need type and link are engine roles, named by the
consumer like the test directives’ (ADR-0009):
symbolneeds_need_types = {"implementation": "impl"} # the defaults
symbolneeds_need_links = {"satisfies": "satisfies"}
Declare both in needs_config.toml; the link’s incoming name is what a
requirement’s page shows:
[[needs.types]]
directive = "impl"
title = "Implementation"
prefix = "IMPL_"
[needs.links.satisfies]
outgoing = "satisfies"
incoming = "satisfied by"
A \satisfies naming a UID that no requirement need has is reported like a
dangling verifies: sphinx-needs warns “unknown outgoing link” in the
stage-2 build (the XML cannot tell, since Doxygen writes a
requirement_<UID> refid for any UID), and Doxygen’s own “Reference to
unknown requirement” warning fails the Doxygen document through
ZDOCS_DOXYGEN_WARN_FAIL_PATTERNS (The ZDOCS_* contract).
Doxylink prefixes
Not a directive at all, but the mechanism every Doxygen document’s symbols are
reached through from Sphinx prose. Every kind: doxygen (and
doxygen-external) registry entry contributes a role named after its own
prefix: (The registry schema), resolved through that document’s
tag file:
See :acme-widget:`acme_widget_init` for the full signature.
This is distinct from :external+<prefix>: (used for a Sphinx peer’s own
labels/objects — see zdocs): a doxylink role has no external+
prefix of its own, because doxylink is not intersphinx and does not share its
role syntax.