Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7dc67d9
feat(module-verification-report): new bundled sphinx extension
antonkri Aug 19, 2026
ab7f2ac
refactor(module-verification-report): split extension into focused mo…
antonkri Aug 19, 2026
01faf8c
test(module-verification-report): add unit tests for the split modules
antonkri Aug 19, 2026
ab711cd
docs(module-verification-report): add extension reference page
antonkri Aug 19, 2026
1d968f5
feat(module-verification-report): annotate testcase back-links with r…
antonkri Aug 19, 2026
60378b2
refactor(module-verification-report): add module-id/feature-id/compon…
antonkri Aug 20, 2026
dc84c33
refactor(module-verification-report): replace filesystem scan with :c…
antonkri Aug 20, 2026
f297ba4
feat(module-verification-report): validate component links at build-f…
antonkri Aug 20, 2026
f52bcde
fix: remove feature-id name guessing — feature section is skipped whe…
antonkri Aug 20, 2026
fbba964
refactor: remove unit test coverage (LCOV) and work-product overrides…
antonkri Aug 20, 2026
30ec2e6
style: apply ruff-format and end-of-file-fixer
antonkri Aug 21, 2026
bbecf47
feat(docs_and_test): add Bazel macro chaining tests/coverage with docs
antonkri Aug 21, 2026
cfd775a
feat(module-verification-report): per-component coverage dropdown
antonkri Aug 21, 2026
e8451d7
refactor(module-verification-report): drop redundant docnames tracking
antonkri Aug 24, 2026
ffaa07a
Add mod_ver_report generation to module-verification-report directive
antonkri Aug 24, 2026
8b75c1a
fix: collapse single-line logger.info call (ruff-format)
antonkri Aug 25, 2026
dc97b63
docs(module-verification-report): document docs_and_test macro
antonkri Aug 25, 2026
05856fa
fix(module-verification-report): omit zero-coverage rows; add -- sepa…
antonkri Aug 25, 2026
41bcf53
refactor: split docs_and_test macro out into #759
antonkri Aug 26, 2026
4666ce2
refactor: remove testcase result annotation, now covered by #739
antonkri Aug 26, 2026
ef19a7d
WIP: Graph checks introduces & covers => features & components
MaximilianSoerenPollak Aug 27, 2026
5a4467e
WIP: testing implementation of template
MaximilianSoerenPollak Aug 27, 2026
1bf4e92
WIP: templates working & some simplification
MaximilianSoerenPollak Aug 27, 2026
3d09fb5
Simplification
MaximilianSoerenPollak Aug 27, 2026
ff524ce
fix: template => post_template
MaximilianSoerenPollak Aug 27, 2026
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
9 changes: 9 additions & 0 deletions src/extensions/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ Extensions
Architecture and design of the ``score_mounts`` bridge extension.
:ref:`Mounts Extension Internals<score_mounts_internals>`

.. grid-item-card::

Module Verification Report
^^^
The ``.. module-verification-report::`` directive that expands
into the standard per-module verification report body.
:ref:`Module Verification Report<module_verification_report>`


.. toctree::
:maxdepth: 1
Expand All @@ -81,3 +89,4 @@ Extensions
Extension Guide <extension_guide>
Sync TOML <sync_toml>
mounts_internals
module_verification_report
179 changes: 179 additions & 0 deletions src/extensions/docs/module_verification_report.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

.. _module_verification_report:

Module Verification Report extension
====================================

``score_module_verification_report`` provides the
``.. module-verification-report::`` directive, which emits the module's
``mod_ver_report`` need. The report body — a feature summary, a component
overview table, and one detailed section per component — is a Sphinx-Needs
content template (``src/needs_templates/mod_ver_report.need``) that the need
selects via ``:template:``. Traceability is resolved by sphinx-needs at render
time: the template only emits ``.. needtable::`` / ``.. needpie::`` widgets
with the right filters.

The extension is part of the :ref:`score_sphinx_bundle<extensions>`.
No external config file is required for the common case.

Typical usage (``verification_report/module_verification_report.rst``):

.. code-block:: rst

.. module-verification-report::
:id: mod_vrep__mymodule__report
:module-id: mod__mymodule
:components: comp__mymodule_a, comp__mymodule_b
:features: feat__mymodule
:safety: QM
:security: YES
:status: valid
:verification-method: test_and_inspection

.. _mvr_directive:

Options
-------

.. list-table::
:header-rows: 1
:widths: 22 12 66

* - Option
- Required
- Description

* - ``:id:``
- yes
- Id of the generated ``mod_ver_report`` need, used verbatim. Must
follow the 3-part scheme the need type requires
(``mod_vrep__<abbrev>__<element>``); ``score_metamodel`` validates it
like any other need id.

* - ``:module-id:``
- yes
- sphinx-needs id of the ``.. mod::`` need (e.g. ``mod__mymodule``).
Also names the module whose component-id prefix
(``comp__<module-short>_``) the template strips to derive component
slugs and titles.

* - ``:components:``
- yes
- Comma-separated list of ``.. comp::`` need ids. Named after the
``components`` link of the ``mod_ver_report`` need type, which it
populates verbatim. Multi-line values are supported. Optional
``[version==N]`` qualifiers are stripped.

* - ``:features:``
- yes
- Comma-separated list of ``.. feat::`` need ids. Named after the
``features`` link of the ``mod_ver_report`` need type, which it
populates verbatim. Usually a single id; one ``Feature`` section is
rendered per entry. Not derived from ``:module-id:`` — guessing a
mandatory traceability link would silently produce a dangling link
whenever the guess is wrong.

* - ``:safety:``
- yes
- ASIL classification of the module. One of ``QM`` or ``ASIL_B``.

* - ``:security:``
- yes
- Whether the module is security-relevant. One of ``YES`` or ``NO``.

* - ``:status:``
- yes
- Review status of the report. One of ``valid`` or ``invalid``.

* - ``:verification-method:``
- yes
- Free-text description of how the module was verified, e.g.
``test_and_inspection``.

* - ``:version:``
- no
- Version of the emitted ``mod_ver_report`` need. Default: ``1``.

Metamodel validation
--------------------

``:safety:``, ``:security:``, ``:status:``, ``:verification-method:`` and
``:version:`` are not just directive options — the directive uses them to
emit a single sphinx-needs ``mod_ver_report`` need (id taken from
``:id:``, linked ``belongs_to`` the module's
``.. mod::`` need). ``:components:`` and ``:features:`` are passed straight
through to the need's ``components`` and ``features`` links, which
``metamodel.yaml`` declares mandatory and types to ``comp`` / ``feat``. This
need type, its id format and the allowed values for each option are declared
in ``score_metamodel``'s ``metamodel.yaml`` (``mod_ver_report`` entry).

Every generated need is checked against that definition by the
``score_metamodel`` Sphinx extension as part of the regular build. If any
value does not match the expected pattern (e.g. ``:safety: ASIL_D``, which
is not one of ``QM``/``ASIL_B``), a mandatory option is missing, or the id
does not follow the required ``<prefix>__<abbreviation>__<element>``
scheme, ``score_metamodel`` reports a warning. Since the documentation
build runs Sphinx with ``-W`` (warnings treated as errors), any such
mismatch aborts the build instead of silently producing an inconsistent
report.

The report template
-------------------

The body lives in ``src/needs_templates/mod_ver_report.need``, a Jinja
template rendered by Sphinx-Needs. Two properties of that mechanism shape it:

*Templates render during the read phase*, when the need is created and the
needs graph does not exist yet. The template therefore never looks other needs
up. It reads ``belongs_to`` / ``components`` / ``features`` off the need
itself, derives component slugs and titles from the ids by string
manipulation, and leaves everything else to ``needtable`` / ``needpie``, which
resolve at write time.

*A need's content cannot open new sections* — docutils rejects them with
"Unexpected section title". The template is therefore applied as
``:post_template:``, not ``:template:``: post-content is placed after the need
at document level, where real section headings work. That is what gives the
report its TOC entries and per-component navigation. Each component section is
additionally a stable link target (``comp-<slug-with-dashes>``).

The heading levels are ``-`` for ``Feature`` and ``Components``, ``~`` for
``Component Overview`` and each component, and ``^`` for the subsections
within a feature or component.

Graph consistency
-----------------

Because the need records which architecture needs the report describes, it can
be cross-checked against them. ``score_metamodel``'s
``check_mod_ver_report_links`` graph check enforces, per report:

#. The need's ``components`` and the module's ``:includes:`` must be the same
set. The report and the module are two independent statements about which
components make up the module; if they disagree, one of them is stale. Both
directions are reported, independently.
#. Every feature a listed component ``belongs_to`` must itself appear in
``:features:``. A report spanning several features is fine; a component
whose feature the report never mentions is not, because the feature-level
statistics would silently omit it.

Ids in ``:components:`` or ``:features:`` that do not resolve to a need are
reported as well. Every problem is reported as a warning rather than raised,
so one build surfaces all of them.

Like every other graph check, it can be disabled or run in isolation via the
``score_metamodel_checks`` config value, e.g.
``score_metamodel_checks = "check_mod_ver_report_links"``.
175 changes: 175 additions & 0 deletions src/extensions/score_metamodel/checks/mod_ver_report_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Graph checks for ``mod_ver_report`` needs.

A ``mod_ver_report`` need declares the module it belongs to (``belongs_to``)
and the architecture needs it describes (``components`` and ``features``, both
mandatory links). Its body is rendered by the ``mod_ver_report`` content
template from those same fields.

Because all of that lives in the needs graph, the report can be validated
against the needs it claims to describe:

1. ``components`` and the module's ``includes`` must name the *same* set. The
report and the module are two independent statements about which components
make up the module — if they disagree, one of them is stale. Both
directions are reported, and independently: a report that skips a component
of its module is exactly as wrong as one that describes a component the
module does not have.
2. Every feature a listed component ``belongs_to`` must itself be listed in
``features``. A report spanning several features is fine — what is not fine
is a component whose feature the report never mentions, because the
feature-level statistics then silently omit it.

Everything here reports through :class:`CheckLogger` rather than raising: a
malformed report must not abort the whole docs build, and the author needs to
see every problem in one run, not just the first.
"""

from __future__ import annotations

from score_metamodel import (
CheckLogger,
graph_check,
)
from sphinx.application import Sphinx
from sphinx_needs.data import NeedsView
from sphinx_needs.need_item import NeedItem


def _linked_ids(need: NeedItem, link: str) -> list[str]:
"""Return the ids linked via *link*, or an empty list.

A declared but unset link yields ``[]``. The ``or []`` also covers a need
type that does not declare *link* at all, where the lookup yields ``None``.
"""
return need.get(link) or []


def _join(ids: list[str]) -> str:
"""Render a list of need ids for a warning message."""
return ", ".join(f"`{i}`" for i in sorted(ids))


def _resolve(
report: NeedItem,
link: str,
all_needs: NeedsView,
log: CheckLogger,
) -> list[NeedItem]:
"""Resolve the ids linked via *link* to needs, warning about unknown ones."""
resolved: list[NeedItem] = []
for need_id in _linked_ids(report, link):
target = all_needs.get(need_id)
if target is None:
log.warning_for_need(
report, f"`{link}` references `{need_id}`, which is not a known need."
)
continue
resolved.append(target)
return resolved


def _check_component_parity(
report: NeedItem, module: NeedItem, log: CheckLogger
) -> None:
"""The report's ``components`` and the module's ``includes`` must match."""
module_components = set(_linked_ids(module, "includes"))
report_components = set(_linked_ids(report, "components"))
module_id = module["id"]

# The two directions are independent problems, so they are reported
# independently — a report that lists a stale component must still be told
# about the component it is missing.
missing_from_report = module_components - report_components
if missing_from_report:
log.warning_for_need(
report,
f"does not list {_join(list(missing_from_report))} under "
f"`components`, but `{module_id}` `includes` "
f"{'them' if len(missing_from_report) > 1 else 'it'}. The "
"verification report must describe every component of the module.",
)

missing_from_module = report_components - module_components
if missing_from_module:
log.warning_for_need(
report,
f"lists {_join(list(missing_from_module))} under `components`, but "
f"`{module_id}` does not `includes` "
f"{'them' if len(missing_from_module) > 1 else 'it'}.",
)


def _check_features_included(
report: NeedItem,
features: list[NeedItem],
components: list[NeedItem],
log: CheckLogger,
) -> None:
"""Every feature a listed component belongs to must be listed too.

A component may belong to more than one feature, and a report may span
more than one feature, so this compares the *full* set of features reached
through the components against the set the report declares.
"""
listed_feature_ids = {feature["id"] for feature in features}

# feature id -> the listed components that belong to it. Keyed by feature
# so the warning can name both the feature that is missing and the
# components that pointed at it.
unlisted: dict[str, list[str]] = {}
for component in components:
for feature_id in _linked_ids(component, "belongs_to"):
if feature_id not in listed_feature_ids:
unlisted.setdefault(feature_id, []).append(component["id"])

for feature_id in sorted(unlisted):
components_str = _join(unlisted[feature_id])
log.warning_for_need(
report,
f"does not list `{feature_id}` under `features`, but "
f"{components_str} "
f"{'belong' if len(unlisted[feature_id]) > 1 else 'belongs'} to it.",
)


@graph_check
def check_mod_ver_report_links(
app: Sphinx,
all_needs: NeedsView,
log: CheckLogger,
) -> None:
"""Validate that every ``mod_ver_report`` agrees with the needs it describes."""
reports = all_needs.filter_is_external(False).filter_types(["mod_ver_report"])

for report in reports.values():
components = _resolve(report, "components", all_needs, log)
features = _resolve(report, "features", all_needs, log)
modules = _resolve(report, "belongs_to", all_needs, log)

_check_features_included(report, features, components, log)

if not modules:
# `belongs_to` is a mandatory link: the option checks report it
# missing, and _resolve already warned about an unresolvable id.
# Nothing left to compare the components against.
continue
if len(modules) > 1:
log.warning_for_need(
report,
f"`belongs_to` names {len(modules)} modules "
f"({_join([m['id'] for m in modules])}); a verification report "
"describes exactly one module.",
)
_check_component_parity(report, modules[0], log)
Loading
Loading