Skip to content

Implementation layer and conditions (4/6) - #7

Merged
tobiaskaestner merged 4 commits into
tkaestner/3-test-report-correctnessfrom
tkaestner/4-implementation-layer
Oct 2, 2026
Merged

tobiaskaestner merged 4 commits into
tkaestner/3-test-report-correctnessfrom
tkaestner/4-implementation-layer

Conversation

@tobiaskaestner

@tobiaskaestner tobiaskaestner commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part 4 of 6 of a stacked series. It is based on #6 (tkaestner/3-test-report-correctness); review the commits below only.

  • symbolneeds: one impl need per API symbol with a \satisfies, linked
    satisfies to its requirement. The registry key is symbol_needs: {doxygen_source: <id>}. The need type and link names belong to the consumer
    (ADR-0009).
  • @kconfig_depends becomes the string field depends_on on test cases and
    impl needs.
  • testmodule_suite_qualifier: a suite group can have a qualified name
    (<module>__<suite>). Two modules can then have tests of the same suite, and
    a suite can have the name of a C function.
  • Each test result says whether its build met the case's depends_on
    (depends_met, from the .config that twister kept), and why it skipped
    (skip_class: config, platform, build-only, unexplained).

Commits

  • 23f98a5 feat: needs: emit a need per API symbol that satisfies a requirement
  • d22ffc7 feat: doxygen: read @kconfig_depends into a depends_on need field
  • 7fa8870 feat: testmodule: derive a need's suite from a qualified group name
  • 333e898 feat: twister: say whether a result's build met its depends_on, and why it skipped

Testing

  • Unit suite (python3 -m pytest sphinx/_extensions/_tests -q) at the head of this PR: 327 passed.
  • The safety docset (about 15 documents, 5 Doxygen projects) builds clean on the top of the stack, with all its gates.
  • The acceptance steps for these changes are in zdocs-tests (one PR after this series). With the engine at the top of the stack, that suite passes 321 of 321.

The stack

  1. Registry and build plumbing (1/6) #4 Registry and build plumbing
  2. Requirement links from Doxygen (2/6) #5 Requirement links from Doxygen
  3. Test report correctness (3/6) #6 Test report correctness
  4. Implementation layer and conditions (4/6) #7 Implementation layer and conditions (this PR)
  5. Incremental rebuilds and see-also (5/6) #8 Incremental rebuilds and see-also
  6. Coverage adequacy (6/6) #9 Coverage adequacy

🤖 Generated with Claude Code

tobiaskaestner and others added 4 commits October 2, 2026 17:07
The API Doxygen project carries Doxygen 1.16 `\satisfies` on its symbols
(191 of them in the safety docset, 300 requirement refs), but the parser
read only `<verifies>`, and the Sphinx API document emitted no needs. A
requirement page could show what verifies it, never what implements it.

A `kind: sphinx` document with a registry block

    symbol_needs:
      doxygen_source: <kind: doxygen id>

now loads a new `symbol_needs` extension. Its `.. symbolneeds::` directive
(optional argument: one Doxygen group; without it the whole project,
sectioned by group or file) emits one need per symbol with `<satisfies>`:
titled with the symbol, id `<TYPE>-<symbol>`, the kind, brief, declaring
file and a link to its Doxygen page in the body, and a link to each
requirement. Type and link are engine roles, `implementation` and
`satisfies`, named by the consumer (defaults `impl`, `satisfies`;
`symbolneeds_need_types` / `symbolneeds_need_links`), as ADR-0009 has it
for the test directives. With the document also publishing
`needs: {source: json}`, a requirement's page lists the symbols under the
link's incoming name next to "verified by". Without the block nothing is
loaded, so existing consumers are unchanged.

A `\satisfies` naming an unknown UID is caught the way a `verifies` is:
Doxygen writes the same `requirement_<UID>` refid for it, so the XML cannot
tell, but sphinx-needs reports the unknown outgoing link, and Doxygen's own
warning trips the stage-2 gate. The fixture is real Doxygen 1.16.1 output
with one such UID, and the test asserts the warning.

The registry validates the block at configure time, docrefs resolves its
XML dir and HTML URL, and add_docs_from_registry adds the same stage-2
edge to the Doxygen document as testmodule's. The input tracking that
testmodule/testreport use moves to its own `input_tracking` extension, so
the new directive re-reads when the XML changes without loading
test_module. doxygen_parser's `\verifies` reading becomes
`requirement_uids(memberdef, relation)`, shared with `\satisfies`. (Also
wraps one over-long line in the parameterized-test tests.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tobias Kaestner <tobias.kaestner@inovex.de>
Safety sources are getting `@kconfig_depends{<condition>}`, an alias for
`\xrefitem kconfig_depends "Depends on" "Kconfig dependencies" \1`, which
a Doxygen input filter adds to code built under a CONFIG condition. A test
case or API function that exists only with CONFIG_ASSERT should say so on
its need, and the parser ignored the xrefsect.

doxygen_parser.kconfig_depends() reads it into `depends_on` on test cases
(parse_memberdef) and API symbols (parse_symbol). The Doxygen 1.16.1 shape,
checked with real runs (fixture fixtures/doxygen-kconfig): adjacent
commands share one xrefsect with a <para> per condition, commands apart get
one xrefsect each, and like @Testid they can sit inside the last list item,
so the whole description is searched by id prefix `kconfig_depends_`. Each
condition is kept verbatim, once, in order, without Doxygen's trailing
space. `&&`, `!` and parentheses survive, and `\,` arrives as `,`. The
@reqref/@Testid loop now skips these xrefsects explicitly, rather than
relying on the id not containing "reqrefs".

Both need builders render the conditions in the body, labelled with the
alias's own title ("Depends on: CONFIG_ASSERT"), because the consumer's
layout decides which fields show. They set the `depends_on` field, the
conditions joined with "; ", only when the consumer's sphinx-needs schema
declares it; otherwise every need would warn "Unknown option". A string
field is the declaration to use. An array field is used only while no
condition contains ; | or , (sphinx-needs splits an array value there, so
`A || B` would become pieces); a need with such a condition gets a
warning instead of a wrong value. That check is in the new needs_fields
module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tobias Kaestner <tobias.kaestner@inovex.de>
A test case need's `suite` was its inner suite group's Doxygen name, and
testreport finds the test case for a twister result by (suite, function)
(SpecLookup). Two test modules can declare the same ZTEST_SUITE: Zephyr's
tests/kernel/workq/user_work and work_queue both declare workqueue_api.
They cannot share one group, because both module pages would then render
every test in it and the need ids would collide. With a group per module,
though, the suite field no longer named the ztest suite, and no result
could find its case.

A new config value `testmodule_suite_qualifier` (default "", today's
behaviour) fixes this. When it is set, e.g. to "__", the suite is the part
of the group name after the qualifier's last occurrence:
kernel_workq_user_work_module__workqueue_api gives workqueue_api. A name
without the qualifier, or with nothing after it, is used whole. A consumer
sets it in the conf.py of the document holding the testmodule directives,
after configure(), like testmodule_need_types. The suite heading's fallback
(a group without a title) uses the derived suite too.

The fallback id of a case without @Testid, testspec-<group>-<function>,
keeps the whole group name, not the suite. Two modules can have a function
of the same name in the same suite (the fixture's test_shared). Scoped by
the suite, both would get testspec-workqueue_api-test_shared, and
sphinx-needs would fail the build with a duplicate id. The group name is
unique by construction. A twister result of such a function is still
ambiguous by (suite, function), and testreport skips it with the existing
warning. That is the same as for any pair two modules document.

The qualifier splits only the group name the suite is derived from. The
(suite, function) key is unchanged, and it now sees the real suite name.
The reference page says so.

Tests: fixtures/doxygen-qualifier (hand-written in Doxygen's shape, two
modules with groups m1__workqueue_api and m2__workqueue_api) and a Sphinx
root that renders both modules on one page. Every need carries suite
workqueue_api, the four ids are distinct, and load_spec_lookup on the
built needs.json maps each module's result to its own case. The unset and
no-occurrence cases are unchanged. On the previous engine the new tests
fail (9 of 11: the helper does not exist, and the suite stays
m1__workqueue_api), and the two unset-qualifier guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tobias Kaestner <tobias.kaestner@inovex.de>
…hy it skipped

A test case's depends_on (@kconfig_depends) says under which Kconfig
condition it is meaningful; a result only linked to its case, so whether
the condition held in the build that produced the result was not
recorded. And a skipped result carried twister's reason only, which does
not tell a skip the specification explains from one it does not.

testreport now sets two result fields:

- depends_met: yes / no / n/a. The case's conditions (all must hold; the
  "; " of depends_on is an and) are evaluated against the .config twister
  keeps for that build, <out>/<platform>/<toolchain>/<test path>/
  <scenario>/zephyr/.config (or the --detailed-test-id layout), looked up
  exactly, never in another scenario's directory. CONFIG_X and
  defined(CONFIG_X) are true when the symbol has any value, not "is not
  set"; !, &&, || and parentheses combine them. n/a for no condition, no
  .config, or a condition outside that grammar (another macro,
  IS_ENABLED(), a comparison): nothing is guessed, and the build warns
  once per case and condition. Each .config read is a tracked input.
- skip_class, on skipped results: build-only (built only), platform (a
  memory region overflowed, or filtered by platform), config (ztest skip
  and depends_met no), unexplained (anything else). A parameterized
  test's class comes from its values' common reason.

Both are field roles with names the consumer may choose
(testreport_need_fields, defaults depends_met / skip_class), merged into
the role->name mapping, and set only where the consumer's sphinx-needs
schema declares the field; ADR-0009 notes the two result fields as the
exception to literal field names. The spec lookup now reads a case's
depends_on (string or array).

On the safety docset (twister-out-b4b, 5386 results): depends_met yes
595, no 424, n/a 4367, no unparseable condition; skipped 610 = config
416, platform 156, build-only 6, unexplained 32; no result ran although
its condition was false.

Tests: fixtures/twister-depends (one module on two boards, .config files
with the feature set on one), the grammar and its rejections, .config
reading and lookup, every class, the assessment of each fixture result,
the field names by role, and a Sphinx root that renames both fields and
checks needs.json and the single warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tobias Kaestner <tobias.kaestner@inovex.de>
@tobiaskaestner
tobiaskaestner force-pushed the tkaestner/4-implementation-layer branch from 93c08dd to 27eca98 Compare October 2, 2026 15:07
@tobiaskaestner
tobiaskaestner merged commit f576c08 into main Oct 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant