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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/extensions/score_metamodel/metamodel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ needs_types:
author: ^.*$
approver: ^.*$
reviewer: ^.*$
# Scopes module_verification_report/platform_verification_report to
# requirements with valid_from <= report_version. Unset means unscoped
# (all requirements shown), used e.g. for the "_latest" reports.
report_version: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$
# req-Id: tool_req__docs_doc_generic_mandatory
mandatory_links:
realizes: workproduct
Expand Down
70 changes: 70 additions & 0 deletions src/extensions/score_sphinx_needs_templates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,72 @@ def __call__(self, need_type: str) -> list[NeedItem]:
_needs_of_type_callable = _NeedsOfType()


def _parse_version(value: str) -> tuple[int, int, int]:
"""Parse a ``valid_from``/``report_version``-style milestone string.

Accepts ``vMAJOR.MINOR`` or ``vMAJOR.MINOR.PATCH`` (e.g. ``v0.8`` or
``v1.0.1``), matching the format enforced by the metamodel for
``valid_from``/``valid_until``/``report_version``.
"""
numbers = [int(part) for part in value.strip().lstrip("vV").split(".")]
while len(numbers) < 3:
numbers.append(0)
return (numbers[0], numbers[1], numbers[2])


class _RequirementInScope:
"""Decide whether a requirement Need belongs to a ``report_version`` scope.

``feat_req`` (and ``stkh_req``) carry ``valid_from`` directly. ``comp_req``
has no ``valid_from`` of its own, so its scope is inherited from the
``feat_req`` Need(s) it is ``derived_from``. A requirement without a
resolvable ``valid_from`` (directly or through ``derived_from``) is
excluded whenever a ``report_version`` scope is active, matching the rule
that only requirements with ``valid_from`` set are considered relevant for
a given release.

Calling with an empty/``None`` ``report_version`` always returns ``True``,
which keeps unscoped reports (e.g. "latest") showing every requirement as
before.
"""

def __call__(self, need: NeedItem, report_version: str | None) -> bool:
if not report_version:
return True

valid_from = need.get("valid_from")
if valid_from:
try:
return _parse_version(valid_from) <= _parse_version(report_version)
except ValueError:
return False
Comment on lines +214 to +219

linked_feat_reqs = _linked_needs_callable(need["id"], "derived_from")
return any(self(feat_req, report_version) for feat_req in linked_feat_reqs)


_req_in_scope_callable = _RequirementInScope()


class _AnyRequirementInScope:
"""Decide whether a Feature/Component has any requirement in scope.

Used to drop an entire Feature/Component section from the report when
``report_version`` is set and none of its requirements qualify, instead of
rendering an empty section. An empty/``None`` ``report_version`` always
returns ``True`` (unscoped reports keep every Feature/Component, even
ones without any requirement at all, as before).
"""

def __call__(self, reqs: list[NeedItem], report_version: str | None) -> bool:
if not report_version:
return True
return any(_req_in_scope_callable(req, report_version) for req in reqs)


_any_req_in_scope_callable = _AnyRequirementInScope()


def _post_templates_requiring_reread(app: Sphinx) -> set[str]:
"""Return post-template names opting into the post-merge rendering pass."""
template_folder = _needs_template_folder()
Expand Down Expand Up @@ -262,6 +328,10 @@ def setup(app: Sphinx) -> dict[str, object]:
)
app.config.needs_render_context.setdefault("linked_needs", _linked_needs_callable)
app.config.needs_render_context.setdefault("needs_of_type", _needs_of_type_callable)
app.config.needs_render_context.setdefault("req_in_scope", _req_in_scope_callable)
app.config.needs_render_context.setdefault(
"any_req_in_scope", _any_req_in_scope_callable
)
app.connect("builder-inited", _capture_build_environment)
# Run after the source-code linker has injected generated testcase Needs and
# their verification backlinks (priority 525), so report templates can
Expand Down
181 changes: 44 additions & 137 deletions src/needs_templates/module_verification_report.need
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
The report is a ``document`` need whose id encodes the module it covers
(``doc__<module>_verification_report``) — the ``document`` type has no
``belongs_to`` link, so the module id is recovered from this need's own id
instead. The module's ``includes`` links provide the components, and each
component's ``belongs_to`` link provides the feature. The ``linked_needs``
helper resolves this graph during the post-collection reread, so titles and
report sections stay driven by the Need model.
instead. The module's ``includes`` links provide the components. The
``linked_needs`` helper resolves this graph during the post-collection
reread, so titles and report sections stay driven by the Need model.

This report is module- and component-scoped only. The platform-wide,
feature-scoped counterpart (all Features and their statistics) lives in the
sibling ``platform_verification_report`` template.

The template is applied as ``:post_template:``, not ``:template:``. A need's
*content* cannot open new sections ("Unexpected section title"), but
Expand All @@ -18,19 +21,21 @@
per-component navigation.
#}
{% set module_id = "mod__" ~ id|replace("doc__", "")|replace("_verification_report", "") %}
{# Unset/empty ``report_version`` keeps the report unscoped (all components
and requirements shown), which is what the "_latest" report relies on. #}
{% set report_version = report_version|default(None, true) %}
{# Resolve the component list from the module's outgoing graph links. A module
may list a component more than once, so deduplicate the NeedItems by ID. #}
{% set components_in_mod = linked_needs(module_id, "includes")|unique(attribute="id")|list %}

{# Collect every feature reachable from the module's components. A feature can
be linked by multiple components, so collect all candidates first and then
keep each feature NeedItem only once in first-seen graph order. The
namespace is required because assignments inside a Jinja loop are scoped. #}
{% set feature_candidates = namespace(items=[]) %}
{% for component in components_in_mod %}
{% set feature_candidates.items = feature_candidates.items + linked_needs(component["id"], "belongs_to") %}
may list a component more than once, so deduplicate the NeedItems by ID.
A component with no requirement in scope is dropped from the report
entirely instead of rendering an empty section. #}
{% set ns_components = namespace(list=[]) %}
{% for component in linked_needs(module_id, "includes")|unique(attribute="id")|list %}
{% set component_reqs = linked_needs(component["id"], "satisfied_by_back")|selectattr("type", "eq", "comp_req")|list %}
{% if any_req_in_scope(component_reqs, report_version) %}
{% set ns_components.list = ns_components.list + [component] %}
{% endif %}
{% endfor %}
{% set report_features = feature_candidates.items|unique(attribute="id")|list %}
{% set components_in_mod = ns_components.list %}

{% set component_workproducts = [
["wp__requirements_inspect", "Requirements Inspection"],
Expand All @@ -39,10 +44,6 @@
["wp__sw_component_dfa", "DFA"],
["wp__sw_component_fmea", "FMEA"],
] %}
{% set feature_workproducts = [
["wp__requirements_inspect", "Requirements Inspection"],
["wp__sw_arch_verification", "Architecture Inspection"],
] %}

{#- One work-product row: the need link, its kind, the realising document and
its status. Both cells are needtables over the same filter, differing only
Expand All @@ -62,6 +63,13 @@
{%- endfor %}
{% endmacro %}

{#- Bracketed, quoted Sphinx-Needs ``id in [...]`` filter literal for a list
of Need IDs, used to scope needpie/needtable filters below to the
requirements that are in scope for ``report_version``. -#}
{% macro id_filter_list(ids) -%}
[{% for req_id in ids %}"{{ req_id }}"{% if not loop.last %}, {% endif %}{% endfor %}]
{%- endmacro %}

.. raw:: html

<style>
Expand All @@ -88,118 +96,6 @@
.wp-doc-table td .dataTables_wrapper .dataTables_paginate { display: none; }
</style>

{#- ===================================================================== -#}
{#- Feature sections resolved from all components' belongs_to links. -#}
{#- ===================================================================== -#}
{% for report_feature in report_features %}
{% set feature_id = report_feature["id"] %}
{% set feature_title = report_feature["title"] %}
{# Derive the work-product document selector from the feature Need reached
through the graph, rather than reconstructing it from the module ID. #}
{% set feature_slug_norm = feature_title|replace("_", "")|replace(" ", "")|lower %}
{% set feature_heading = feature_title if report_features|length == 1 else "Feature: " ~ feature_title %}

{{ feature_heading }}
{{ "-" * (feature_heading|length) }}

.. needtable::
:filter: id == "{{ feature_id }}"
:columns: title as "Name";id as "Id";safety;security;status
:style: table

Requirements Statistics
~~~~~~~~~~~~~~~~~~~~~~~

.. grid:: 1 2 2 2
:gutter: 3

.. grid-item::

.. needpie:: Feature Requirements Status
:labels: valid, invalid
:colors: #37a12d, #ca2828
:legend:

type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid"
type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "invalid"

.. grid-item::

.. needpie:: Feature Requirements Test Coverage
:labels: fully covered, partially covered, not covered
:colors: #37a12d, #f0a500, #ca2828
:legend:

type == "feat_req" and "{{ feature_id }}" in satisfied_by and fully_verifies_back
type == "feat_req" and "{{ feature_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back
type == "feat_req" and "{{ feature_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back

.. dropdown:: Show requirements table
:animate: fade-in

.. needtable::
:filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by
:style: table
:columns: id;title;safety;status;testlink
:colwidths: 13,22,8,10,47
:sort: id

Architecture Statistics
~~~~~~~~~~~~~~~~~~~~~~~

.. grid:: 1 2 2 2
:gutter: 3

.. grid-item::

.. needpie:: Feature Architecture Elements Status
:labels: valid, invalid
:colors: #37a12d, #ca2828
:legend:

type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "valid"
type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "invalid"

.. grid-item::

.. needpie:: Feature Architecture Elements Inspection Status
:labels: inspected, not inspected
:colors: #37a12d, #ca2828
:legend:

type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and "inspected" in tags
type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and "inspected" not in tags

.. dropdown:: Show architectural elements table
:animate: fade-in

.. needtable::
:filter: type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to
:style: table
:columns: id;title;safety;status;tags
:colwidths: 25,30,10,15,20
:sort: id

Inspection Statistics
~~~~~~~~~~~~~~~~~~~~~

Presence of the feature-level inspection work products.

.. dropdown:: Show work products table
:animate: fade-in

.. list-table::
:header-rows: 1
:widths: 30 25 25 20
:class: wp-doc-table

* - Work Product
- Kind
- Realized by
- Status
{{ workproduct_rows(feature_slug_norm, feature_workproducts) }}
{% endfor %}

{# ===================================================================== #}
{# Components #}
{# ===================================================================== #}
Expand Down Expand Up @@ -233,6 +129,17 @@ Component Overview

<hr style="border-top: 2px solid #333333; margin: 0.5em 0 1.5em 0;">

{% set component_reqs_all = linked_needs(component_id, "satisfied_by_back")
|selectattr("type", "eq", "comp_req")
|list %}
{% set ns = namespace(req_ids=[]) %}
{% for req in component_reqs_all %}
{% if req_in_scope(req, report_version) %}
{% set ns.req_ids = ns.req_ids + [req["id"]] %}
{% endif %}
{% endfor %}
{% set component_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %}

Component Requirements Statistics
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -246,8 +153,8 @@ Component Requirements Statistics
:colors: #37a12d, #ca2828
:legend:

type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "valid"
type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "invalid"
type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "valid" and {{ component_req_filter }}
type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "invalid" and {{ component_req_filter }}

.. grid-item::

Expand All @@ -256,9 +163,9 @@ Component Requirements Statistics
:colors: #37a12d, #f0a500, #ca2828
:legend:

type == "comp_req" and "{{ component_id }}" in satisfied_by and fully_verifies_back
type == "comp_req" and "{{ component_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back
type == "comp_req" and "{{ component_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back
type == "comp_req" and "{{ component_id }}" in satisfied_by and fully_verifies_back and {{ component_req_filter }}
type == "comp_req" and "{{ component_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back and {{ component_req_filter }}
type == "comp_req" and "{{ component_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back and {{ component_req_filter }}

Component Architecture Statistics
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -296,7 +203,7 @@ verification status and the tests that (fully or partially) verify them:
:animate: fade-in

.. needtable::
:filter: type == "comp_req" and "{{ component_id }}" in satisfied_by
:filter: type == "comp_req" and "{{ component_id }}" in satisfied_by and {{ component_req_filter }}
:style: table
:columns: id;title;safety;status;testlink
:colwidths: 13,22,8,10,47
Expand Down
Loading
Loading