diff --git a/src/extensions/score_sphinx_needs_templates/README.md b/src/extensions/score_sphinx_needs_templates/README.md index fa76b5a7a..31d0e8111 100644 --- a/src/extensions/score_sphinx_needs_templates/README.md +++ b/src/extensions/score_sphinx_needs_templates/README.md @@ -25,6 +25,8 @@ The extension provides: directory; * the `linked_needs(need_id, link_name)` helper for traversing Need links; * the `needs_of_type(need_type)` helper for selecting Needs by type; +* the `id_filter_list`, `workproduct_rows` and `safety_pie_row` helpers, which + render RST fragments shared by several report templates; * support for graph-driven `post_template`s that are rendered after parallel Need collection has been merged; * ordinary Sphinx page navigation for sections generated by those diff --git a/src/extensions/score_sphinx_needs_templates/__init__.py b/src/extensions/score_sphinx_needs_templates/__init__.py index 806adb609..d7f3f44d8 100644 --- a/src/extensions/score_sphinx_needs_templates/__init__.py +++ b/src/extensions/score_sphinx_needs_templates/__init__.py @@ -290,6 +290,178 @@ def __call__(self, ids: list[str], referenced: list[str]) -> dict[str, list[str] _split_by_reference_callable = _SplitByReference() +class _IdFilterList: + """Render Need IDs as the quoted ``[...]`` literal of an ``id in`` filter.""" + + def __call__(self, ids: list[str]) -> str: + return "[" + ", ".join(f'"{need_id}"' for need_id in ids) + "]" + + +_id_filter_list_callable = _IdFilterList() + + +class _WorkproductRows: + """Render work-product rows for a six-space indented ``list-table``. + + Each row links the work product, its kind, the realising document and its + status. Both document cells are needtables over the same filter, differing + only in ``:columns:``, so a missing document renders as an empty cell. + """ + + def __call__(self, slug_norm: str, workproducts: list[list[str]]) -> str: + rows: list[str] = [] + for wp_id, kind in workproducts: + doc_filter = ( + f'type == "document" and "{slug_norm}" in id.replace("_", "").lower()' + f' and "{wp_id}" in realizes' + ) + rows.append( + "\n".join( + [ + f" * - :need:`{wp_id}`", + f" - {kind}", + " - .. needtable::", + f" :filter: {doc_filter}", + " :columns: id", + " :style: table", + " - .. needtable::", + f" :filter: {doc_filter}", + " :columns: status", + " :style: table", + ] + ) + ) + return "".join(f"\n{row}" for row in rows) + "\n" + + +_workproduct_rows_callable = _WorkproductRows() + + +class _SafetyPieRow: + """Render one row of pies restricted to a single safety class. + + Shared by the module and platform verification reports, so that QM and + ASIL figures are never mixed in one chart. QM Needs are not formally + inspected, so there ``valid`` already is the green "done" slice rather + than the yellow "not inspected yet" one; a QM Need that is nevertheless + ``valid_inspected`` counts as ``valid``, as in the metamodel graph checks. + + ``with_reference`` adds a third, reference-coverage pie; ``referenced`` and + ``missing`` must already be partitioned over this safety class alone. The + raster stays at three columns either way, so that a two-pie row renders + its charts at exactly the same size as a three-pie one. + + ``count`` decides between pies and placeholders only; it is deliberately + not shown, because it is resolved through the Need graph while the pies + count through Sphinx-Needs filters, and the two can disagree on links + carrying a ``[version==N]`` selector. + + This is a render helper instead of a Jinja macro because the MiniJinja + environment has no template loader, so macros cannot be shared between + templates. + """ + + def __call__( + self, + title: str, + subject: str, + base: str, + safety: str, + safety_label: str, + count: int, + with_reference: bool = False, + referenced: list[str] | None = None, + missing: list[str] | None = None, + ) -> str: + scope = f'{base} and safety == "{safety}"' + # (slice label, status condition) per pie slice; green "done" comes first. + statuses = ( + [ + ("valid", '(status == "valid" or status == "valid_inspected")'), + ("invalid", 'status == "invalid"'), + ] + if safety == "QM" + else [ + (state, f'status == "{state}"') + for state in ("valid_inspected", "valid", "invalid") + ] + ) + status_colors = ( + "#37a12d, #ca2828" if safety == "QM" else "#37a12d, #f5c518, #ca2828" + ) + # docutils normalises ``:class:`` values, so avoid underscores in them. + safety_css = safety.lower().replace("_", "-") + chart_prefix = f"{title} {subject}".strip() + + lines = [ + "", + f".. rubric:: {safety_label} — {subject}", + f" :class: safety-class-heading safety-{safety_css}", + "", + ".. grid:: 1 2 3 3", + " :gutter: 2", + " :class-row: score-pie-row", + ] + + def grid_item(name: str, pie: list[str]) -> None: + lines.extend(["", " .. grid-item::", ""]) + if count == 0: + lines.extend( + [ + " .. raw:: html", + "", + '
', + f' {subject} {name} ({safety_label})', + f' no {safety_label} {subject.lower()}', + "
", + ] + ) + else: + lines.append( + f" .. needpie:: {chart_prefix} {name} ({safety_label})" + ) + lines.extend(f" {line}".rstrip() for line in pie) + + grid_item( + "Status", + [ + f":labels: {', '.join(label for label, _ in statuses)}", + f":colors: {status_colors}", + ":legend:", + "", + *(f"{scope} and {condition}" for _, condition in statuses), + ], + ) + grid_item( + "Test Coverage", + [ + ":labels: fully covered, partially covered, not covered", + ":colors: #37a12d, #f0a500, #ca2828", + ":legend:", + "", + f"{scope} and fully_verifies_back", + f"{scope} and partially_verifies_back and not fully_verifies_back", + f"{scope} and not fully_verifies_back and not partially_verifies_back", + ], + ) + if with_reference: + grid_item( + "Reference Coverage", + [ + ":labels: referenced by lower, not referenced", + ":colors: #37a12d, #ca2828", + ":legend:", + "", + f"id in {_id_filter_list_callable(referenced or [])}", + f"id in {_id_filter_list_callable(missing or [])}", + ], + ) + return "\n".join(lines) + "\n" + + +_safety_pie_row_callable = _SafetyPieRow() + + 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() @@ -386,6 +558,15 @@ def setup(app: Sphinx) -> dict[str, object]: app.config.needs_render_context.setdefault( "split_by_reference", _split_by_reference_callable ) + app.config.needs_render_context.setdefault( + "id_filter_list", _id_filter_list_callable + ) + app.config.needs_render_context.setdefault( + "workproduct_rows", _workproduct_rows_callable + ) + app.config.needs_render_context.setdefault( + "safety_pie_row", _safety_pie_row_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 diff --git a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py index bddb56735..641cde114 100644 --- a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py +++ b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py @@ -173,6 +173,69 @@ def test_any_req_in_report_version_false_if_no_requirement_matches() -> None: assert any_req_in_report_version([a, b], "v1.0") is False +def test_safety_pie_row_qm_counts_inspected_as_valid() -> None: + """QM has no inspected slice; ``valid_inspected`` QM Needs count as valid.""" + safety_pie_row = vars(templates)["_SafetyPieRow"]() + + qm = safety_pie_row("Comp", "Requirements", "BASE", "QM", "QM", 1) + asil = safety_pie_row("Comp", "Requirements", "BASE", "ASIL_B", "ASIL B", 1) + + assert ":labels: valid, invalid" in qm + assert ( + 'BASE and safety == "QM" and (status == "valid" or status == "valid_inspected")' + ) in qm + assert 'BASE and safety == "QM" and status == "invalid"' in qm + assert ":labels: valid_inspected, valid, invalid" in asil + assert ":colors: #37a12d, #f5c518, #ca2828" in asil + assert 'BASE and safety == "ASIL_B" and status == "valid_inspected"' in asil + assert ":class: safety-class-heading safety-asil-b" in asil + + +def test_safety_pie_row_empty_class_renders_placeholders_only() -> None: + safety_pie_row = vars(templates)["_SafetyPieRow"]() + + row = safety_pie_row("Comp", "Requirements", "BASE", "QM", "QM", 0, True) + + assert "needpie" not in row + assert row.count('
') == 3 + assert "no QM requirements" in row + + +def test_safety_pie_row_reference_pie_only_on_request() -> None: + safety_pie_row = vars(templates)["_SafetyPieRow"]() + + without = safety_pie_row("", "Stakeholder Requirements", "B", "QM", "QM", 2) + with_ref = safety_pie_row( + "", "Stakeholder Requirements", "B", "QM", "QM", 2, True, ["a", "b"], ["c"] + ) + + assert "Reference Coverage" not in without + assert without.count(".. needpie::") == 2 + assert with_ref.count(".. needpie::") == 3 + assert 'id in ["a", "b"]' in with_ref + assert 'id in ["c"]' in with_ref + # An empty title must not leave a leading blank in the chart title. + assert ".. needpie:: Stakeholder Requirements Status (QM)" in with_ref + + +def test_id_filter_list_renders_quoted_literal() -> None: + id_filter_list = vars(templates)["_IdFilterList"]() + + assert id_filter_list(["a", "b"]) == '["a", "b"]' + assert id_filter_list([]) == "[]" + + +def test_workproduct_rows_filter_by_slug_and_work_product() -> None: + workproduct_rows = vars(templates)["_WorkproductRows"]() + + rows = workproduct_rows("mycomp", [["wp__dfa", "DFA"], ["wp__fmea", "FMEA"]]) + + assert rows.count(" * - :need:`") == 2 + assert " - DFA\n" in rows + assert rows.count('"mycomp" in id.replace("_", "").lower()') == 4 + assert rows.count('and "wp__fmea" in realizes') == 2 + + def test_tool_qualification_matrix_groups_shared_requirements( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/src/needs_templates/module_verification_report.need b/src/needs_templates/module_verification_report.need index 80377f442..6df98553a 100644 --- a/src/needs_templates/module_verification_report.need +++ b/src/needs_templates/module_verification_report.need @@ -53,30 +53,8 @@ ["wp__sw_component_fmea", "FMEA"], ] %} -{#- 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 - in :columns:, so an empty match renders as an empty cell. -#} -{% macro workproduct_rows(slug_norm, workproducts) %} -{%- for wp in workproducts %} - * - :need:`{{ wp[0] }}` - - {{ wp[1] }} - - .. needtable:: - :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes - :columns: id - :style: table - - .. needtable:: - :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes - :columns: status - :style: table -{%- 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 ``applies_to_module_version``. -#} -{% macro id_filter_list(ids) -%} -[{% for req_id in ids %}"{{ req_id }}"{% if not loop.last %}, {% endif %}{% endfor %}] -{%- endmacro %} +{#- ``workproduct_rows``, ``id_filter_list`` and ``safety_pie_row`` are render + helpers shared with the platform report. -#} .. raw:: html @@ -102,8 +80,74 @@ .wp-doc-table td .dataTables_wrapper .dataTables_filter, .wp-doc-table td .dataTables_wrapper .dataTables_info, .wp-doc-table td .dataTables_wrapper .dataTables_paginate { display: none; } + + /* Scales every chart to its column; together with the fixed three-column + raster this gives every chart the same size as in the platform report. */ + .score-pie-row img[id^="needpie-"] { + width: 100%; + height: auto; + } + /* ``.sd-row`` is a flex container without a justification of its own, so + this centres the two pies in the three-column raster. */ + .score-pie-row { justify-content: center; } + + p.rubric.safety-class-heading { + display: inline-block; margin: 1.4em 0 0.5em 0; + padding: 0.15em 0.9em; border-radius: 1em; + font-size: 0.95em; font-weight: 600; color: #ffffff; + } + p.rubric.safety-class-heading.safety-asil-b { background: #1f4e79; } + p.rubric.safety-class-heading.safety-qm { background: #6c757d; } + + /* Matches the aspect ratio of the rendered pie charts. */ + .empty-pie { + display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 0.4em; + aspect-ratio: 16 / 9; padding: 0.5em; text-align: center; + border: 2px dashed #b0b0b0; border-radius: 6px; color: #6c757d; + } + .empty-pie-title { font-weight: 600; } + .empty-pie-text { font-style: italic; } + + .chart-legend { display: flex; flex-wrap: wrap; gap: 1.2em 2.5em; } + .chart-legend-group { + display: flex; flex-direction: column; gap: 0.3em; + flex: 1 1 20em; font-size: 0.9em; + } + .chart-legend-head { font-weight: 600; } + .chart-legend-dot { + display: inline-block; width: 0.8em; height: 0.8em; + margin-right: 0.5em; border-radius: 50%; + border: 1px solid rgba(0, 0, 0, 0.25); + } +.. dropdown:: Legend — status values and chart colours + :animate: fade-in + :open: + + .. raw:: html + +
+
+ Status — ASIL B + valid_inspected — agreed and formally inspected + valid — agreed, formal inspection still outstanding + invalid — not agreed; metamodel checks are skipped +
+
+ Status — QM + valid — agreed; QM needs are not formally inspected, so this is their final state (valid_inspected counts here too) + invalid — not agreed; metamodel checks are skipped +
+
+ Test coverage — both classes + fully covered — at least one test fully verifies the need + partially covered — only partially verifying tests + not covered — no verifying test at all +
+
+ {# ===================================================================== #} {# Components #} {# ===================================================================== #} @@ -118,7 +162,7 @@ Component Overview :filter: id in [{% for component in components_in_mod %}"{{ component["id"] }}"{% if not loop.last %}, {% endif %}{% endfor %}] :columns: id as "Component";safety;security;status :style: table - :sort: id + :sort: safety {% for component in components_in_mod %} {% set component_id = component["id"] %} @@ -140,69 +184,35 @@ Component Overview {% set component_reqs_all = linked_needs(component_id, "satisfied_by_back") |selectattr("type", "eq", "comp_req") |list %} -{% set ns = namespace(req_ids=[]) %} +{% set ns = namespace(reqs=[]) %} {% for req in component_reqs_all %} {% if req_in_report_version(req, module_version) %} -{% set ns.req_ids = ns.req_ids + [req["id"]] %} +{% set ns.reqs = ns.reqs + [req] %} {% endif %} {% endfor %} -{% set component_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %} +{% set component_reqs = ns.reqs %} +{% set component_req_filter = 'id in ' ~ id_filter_list(component_reqs|map(attribute="id")|list) %} +{#- ``type`` has no "in" test in minijinja, so the two architecture types are + collected separately and concatenated. -#} +{% set component_arcs_all = linked_needs(component_id, "belongs_to_back") %} +{% set component_arcs = (component_arcs_all|selectattr("type", "eq", "comp_arc_sta")|list) + + (component_arcs_all|selectattr("type", "eq", "comp_arc_dyn")|list) %} Component Requirements Statistics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. grid:: 1 2 2 2 - :gutter: 3 - - .. grid-item:: - - .. needpie:: {{ component_title }} Requirements Status - :labels: valid, valid_inspected, invalid - :colors: #f5c518, #37a12d, #ca2828 - :legend: - - 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 == "valid_inspected" and {{ component_req_filter }} - type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "invalid" and {{ component_req_filter }} - - .. grid-item:: - - .. needpie:: {{ component_title }} Requirements Test Coverage - :labels: fully covered, partially covered, not covered - :colors: #37a12d, #f0a500, #ca2828 - :legend: - - 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 }} +{% set req_base = 'type == "comp_req" and "' ~ component_id ~ '" in satisfied_by and ' ~ component_req_filter %} +{{ safety_pie_row(component_title, "Requirements", req_base, "ASIL_B", "ASIL B", + component_reqs|selectattr("safety", "eq", "ASIL_B")|list|length) }} +{{ safety_pie_row(component_title, "Requirements", req_base, "QM", "QM", + component_reqs|selectattr("safety", "eq", "QM")|list|length) }} Component Architecture Statistics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. grid:: 1 2 2 2 - :gutter: 3 - - .. grid-item:: - - .. needpie:: {{ component_title }} Architecture Elements Status - :labels: valid, valid_inspected, invalid - :colors: #f5c518, #37a12d, #ca2828 - :legend: - - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "valid" - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "valid_inspected" - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "invalid" - - .. grid-item:: - - .. needpie:: {{ component_title }} Architecture Elements Test Coverage - :labels: fully covered, partially covered, not covered - :colors: #37a12d, #f0a500, #ca2828 - :legend: - - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and fully_verifies_back - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and partially_verifies_back and not fully_verifies_back - type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and not fully_verifies_back and not partially_verifies_back +{% set arc_base = 'type in ["comp_arc_sta", "comp_arc_dyn"] and "' ~ component_id ~ '" in belongs_to' %} +{{ safety_pie_row(component_title, "Architecture Elements", arc_base, "ASIL_B", "ASIL B", + component_arcs|selectattr("safety", "eq", "ASIL_B")|list|length) }} +{{ safety_pie_row(component_title, "Architecture Elements", arc_base, "QM", "QM", + component_arcs|selectattr("safety", "eq", "QM")|list|length) }} Requirements Traceability ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -218,7 +228,7 @@ verification status and the tests that (fully or partially) verify them: :style: table :columns: id;title;safety;status;testlink :colwidths: 13,22,8,10,47 - :sort: id + :sort: safety Test Coverage ^^^^^^^^^^^^^ @@ -251,7 +261,7 @@ status have not yet been inspected. :style: table :columns: id;title;safety;status;tags :colwidths: 25,30,10,15,20 - :sort: id + :sort: safety Verification & Safety Analysis Documents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 9f6b30aad..62efcf1e8 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -37,40 +37,19 @@ ["wp__feature_fmea", "FMEA"], ] %} -{#- 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 - in :columns:, so an empty match renders as an empty cell. -#} -{% macro workproduct_rows(slug_norm, workproducts) %} -{%- for wp in workproducts %} - * - :need:`{{ wp[0] }}` - - {{ wp[1] }} - - .. needtable:: - :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes - :columns: id - :style: table - - .. needtable:: - :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes - :columns: status - :style: table -{%- 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 %} +{#- ``workproduct_rows``, ``id_filter_list`` and ``safety_pie_row`` are render + helpers shared with the module report. -#} {#- Stakeholder requirements are not scoped to a Feature, so they are collected once for the whole report rather than per-Feature like ``feat_req``. -#} {% set ns_stkh_reqs = namespace(list=[]) %} {% for req in needs_of_type("stkh_req")|unique(attribute="id")|list %} {% if req_in_report_version(req, report_version) %} -{% set ns_stkh_reqs.list = ns_stkh_reqs.list + [req["id"]] %} +{% set ns_stkh_reqs.list = ns_stkh_reqs.list + [req] %} {% endif %} {% endfor %} -{% set stkh_req_filter = 'id in ' ~ id_filter_list(ns_stkh_reqs.list) %} +{% set stkh_req_ids = ns_stkh_reqs.list|map(attribute="id")|list %} +{% set stkh_req_filter = 'id in ' ~ id_filter_list(stkh_req_ids) %} .. raw:: html +.. dropdown:: Legend — status values and chart colours + :animate: fade-in + :open: + + .. raw:: html + +
+
+ Status — ASIL B + valid_inspected — agreed and formally inspected + valid — agreed, formal inspection still outstanding + invalid — not agreed; metamodel checks are skipped +
+
+ Status — QM + valid — agreed; QM needs are not formally inspected, so this is their final state (valid_inspected counts here too) + invalid — not agreed; metamodel checks are skipped +
+
+ Test coverage — both classes + fully covered — at least one test fully verifies the need + partially covered — only partially verifying tests + not covered — no verifying test at all +
+
+ Reference coverage — both classes + referenced by lower — a requirement one level down derives from it + not referenced — nothing on the level below derives from it +
+
+ {# ===================================================================== #} {# Stakeholder Requirements #} {# ===================================================================== #} @@ -113,44 +157,17 @@ Stakeholder Requirements one in-scope feature requirement is ``derived_from`` it. The index is built and applied in Python, because every ``in`` test evaluated here would cross the Rust/Python boundary once per comparison. -#} -{% set ns_stkh_ref = split_by_reference( - ns_stkh_reqs.list, - referenced_ids("feat_req", "derived_from", report_version)) %} -.. grid:: 1 2 2 2 - :gutter: 3 - - .. grid-item:: - - .. needpie:: Stakeholder Requirements Status - :labels: valid, valid_inspected, invalid - :colors: #f5c518, #37a12d, #ca2828 - :legend: - - type == "stkh_req" and status == "valid" and {{ stkh_req_filter }} - type == "stkh_req" and status == "valid_inspected" and {{ stkh_req_filter }} - type == "stkh_req" and status == "invalid" and {{ stkh_req_filter }} - - .. grid-item:: - - .. needpie:: Stakeholder Requirements Test Coverage - :labels: fully covered, partially covered, not covered - :colors: #37a12d, #f0a500, #ca2828 - :legend: - - type == "stkh_req" and fully_verifies_back and {{ stkh_req_filter }} - type == "stkh_req" and partially_verifies_back and not fully_verifies_back and {{ stkh_req_filter }} - type == "stkh_req" and not fully_verifies_back and not partially_verifies_back and {{ stkh_req_filter }} - - .. grid-item:: - :class: score-centered-grid-item - - .. needpie:: Stakeholder Requirements Reference Coverage - :labels: referenced by lower, not referenced - :colors: #37a12d, #ca2828 - :legend: - - id in {{ id_filter_list(ns_stkh_ref.referenced) }} - id in {{ id_filter_list(ns_stkh_ref.missing) }} +{% set stkh_referenced_ids = referenced_ids("feat_req", "derived_from", report_version) %} +{% set ns_stkh_ref = split_by_reference(stkh_req_ids, stkh_referenced_ids) %} +{% set stkh_base = 'type == "stkh_req" and ' ~ stkh_req_filter %} +{% set stkh_asil = ns_stkh_reqs.list|selectattr("safety", "eq", "ASIL_B")|list %} +{% set stkh_qm = ns_stkh_reqs.list|selectattr("safety", "eq", "QM")|list %} +{% set stkh_asil_ref = split_by_reference(stkh_asil|map(attribute="id")|list, stkh_referenced_ids) %} +{% set stkh_qm_ref = split_by_reference(stkh_qm|map(attribute="id")|list, stkh_referenced_ids) %} +{{ safety_pie_row("", "Stakeholder Requirements", stkh_base, "ASIL_B", "ASIL B", + stkh_asil|length, true, stkh_asil_ref.referenced, stkh_asil_ref.missing) }} +{{ safety_pie_row("", "Stakeholder Requirements", stkh_base, "QM", "QM", + stkh_qm|length, true, stkh_qm_ref.referenced, stkh_qm_ref.missing) }} .. dropdown:: Show requirements table :animate: fade-in @@ -160,9 +177,9 @@ Stakeholder Requirements :style: table :columns: id;title;safety;status;testlink :colwidths: 13,22,8,10,47 - :sort: id + :sort: safety {% if ns_stkh_ref.missing %} -{#- Detail view of the "not referenced" slice of the reference coverage pie +{#- Detail view of the "not referenced" slices of the reference coverage pies above; both are rendered from the same in-scope ID list. -#} .. dropdown:: Not referenced by feature requirements ({{ ns_stkh_ref.missing|length }}) @@ -174,7 +191,7 @@ Stakeholder Requirements :style: table :columns: id;title;safety;status :colwidths: 15,45,15,25 - :sort: id + :sort: safety {% endif %} {# ===================================================================== #} @@ -191,7 +208,7 @@ Feature Overview :filter: id in [{% for feature in report_features %}"{{ feature["id"] }}"{% if not loop.last %}, {% endif %}{% endfor %}] :columns: id as "Feature";safety;security;status :style: table - :sort: id + :sort: safety {#- Reference coverage index, built once for all Features: a feature requirement is "referenced" when at least one in-scope component @@ -217,54 +234,34 @@ Feature Overview {% set feature_reqs_all = linked_needs(feature_id, "satisfied_by_back") |selectattr("type", "eq", "feat_req") |list %} -{% set ns = namespace(req_ids=[]) %} +{% set ns = namespace(reqs=[]) %} {% for req in feature_reqs_all %} {% if req_in_report_version(req, report_version) %} -{% set ns.req_ids = ns.req_ids + [req["id"]] %} +{% set ns.reqs = ns.reqs + [req] %} {% endif %} {% endfor %} -{% set feature_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %} +{% set feature_reqs = ns.reqs %} +{% set feature_req_ids = feature_reqs|map(attribute="id")|list %} +{% set feature_req_filter = 'id in ' ~ id_filter_list(feature_req_ids) %} {#- Reference coverage: partition this Feature's in-scope requirements against the index built once above. -#} -{% set ns_feat_ref = split_by_reference(ns.req_ids, comp_referenced_ids) %} +{% set ns_feat_ref = split_by_reference(feature_req_ids, comp_referenced_ids) %} +{#- ``type`` has no "in" test in minijinja, so the two architecture types are + collected separately and concatenated. -#} +{% set feature_arcs_all = linked_needs(feature_id, "belongs_to_back") %} +{% set feature_arcs = (feature_arcs_all|selectattr("type", "eq", "feat_arc_sta")|list) + + (feature_arcs_all|selectattr("type", "eq", "feat_arc_dyn")|list) %} Requirements Statistics ^^^^^^^^^^^^^^^^^^^^^^^ - -.. grid:: 1 2 2 2 - :gutter: 3 - - .. grid-item:: - - .. needpie:: {{ feature_title }} Requirements Status - :labels: valid, valid_inspected, invalid - :colors: #f5c518, #37a12d, #ca2828 - :legend: - - type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid" and {{ feature_req_filter }} - type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid_inspected" and {{ feature_req_filter }} - type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "invalid" and {{ feature_req_filter }} - - .. grid-item:: - - .. needpie:: {{ feature_title }} 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 and {{ feature_req_filter }} - type == "feat_req" and "{{ feature_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back and {{ feature_req_filter }} - type == "feat_req" and "{{ feature_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back and {{ feature_req_filter }} - - .. grid-item:: - :class: score-centered-grid-item - - .. needpie:: {{ feature_title }} Requirements Reference Coverage - :labels: referenced by lower, not referenced - :colors: #37a12d, #ca2828 - :legend: - - id in {{ id_filter_list(ns_feat_ref.referenced) }} - id in {{ id_filter_list(ns_feat_ref.missing) }} +{% set req_base = 'type == "feat_req" and "' ~ feature_id ~ '" in satisfied_by and ' ~ feature_req_filter %} +{% set feat_asil = feature_reqs|selectattr("safety", "eq", "ASIL_B")|list %} +{% set feat_qm = feature_reqs|selectattr("safety", "eq", "QM")|list %} +{% set feat_asil_ref = split_by_reference(feat_asil|map(attribute="id")|list, comp_referenced_ids) %} +{% set feat_qm_ref = split_by_reference(feat_qm|map(attribute="id")|list, comp_referenced_ids) %} +{{ safety_pie_row(feature_title, "Requirements", req_base, "ASIL_B", "ASIL B", + feat_asil|length, true, feat_asil_ref.referenced, feat_asil_ref.missing) }} +{{ safety_pie_row(feature_title, "Requirements", req_base, "QM", "QM", + feat_qm|length, true, feat_qm_ref.referenced, feat_qm_ref.missing) }} .. dropdown:: Show requirements table :animate: fade-in @@ -274,7 +271,7 @@ Requirements Statistics :style: table :columns: id;title;safety;status;testlink :colwidths: 13,22,8,10,47 - :sort: id + :sort: safety {% if ns_feat_ref.missing %} .. dropdown:: Not referenced by component requirements ({{ ns_feat_ref.missing|length }}) @@ -286,36 +283,16 @@ Requirements Statistics :style: table :columns: id;title;safety;status :colwidths: 15,45,15,25 - :sort: id + :sort: safety {% endif %} Architecture Statistics ^^^^^^^^^^^^^^^^^^^^^^^ - -.. grid:: 1 2 2 2 - :gutter: 3 - - .. grid-item:: - - .. needpie:: {{ feature_title }} Architecture Elements Status - :labels: valid, valid_inspected, invalid - :colors: #f5c518, #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 == "valid_inspected" - type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "invalid" - - .. grid-item:: - - .. needpie:: {{ feature_title }} Architecture Elements Test Coverage - :labels: fully covered, partially covered, not covered - :colors: #37a12d, #f0a500, #ca2828 - :legend: - - type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and fully_verifies_back - type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and partially_verifies_back and not fully_verifies_back - type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and not fully_verifies_back and not partially_verifies_back +{% set arc_base = 'type in ["feat_arc_sta", "feat_arc_dyn"] and "' ~ feature_id ~ '" in belongs_to' %} +{{ safety_pie_row(feature_title, "Architecture Elements", arc_base, "ASIL_B", "ASIL B", + feature_arcs|selectattr("safety", "eq", "ASIL_B")|list|length, false, [], []) }} +{{ safety_pie_row(feature_title, "Architecture Elements", arc_base, "QM", "QM", + feature_arcs|selectattr("safety", "eq", "QM")|list|length, false, [], []) }} .. dropdown:: Show architectural elements table :animate: fade-in @@ -325,7 +302,7 @@ Architecture Statistics :style: table :columns: id;title;safety;status;tags :colwidths: 25,30,10,15,20 - :sort: id + :sort: safety Inspection Statistics ^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/needs_templates/tool_qualification_report.need b/src/needs_templates/tool_qualification_report.need index d27cbf9ff..ff2c2bbe5 100644 --- a/src/needs_templates/tool_qualification_report.need +++ b/src/needs_templates/tool_qualification_report.need @@ -29,10 +29,6 @@ {{ ((value|default("", true)|replace("\n", " ")|trim) or fallback)|replace("\\", "\\\\")|replace('"', '\\"') }} {%- endmacro %} -{% macro id_filter_list(ids) -%} -[{% for need_id in ids %}"{{ need_id }}"{% if not loop.last %}, {% endif %}{% endfor %}] -{%- endmacro %} - {% set usecase_malfunction_scope = namespace(with_malfunctions=[], without_malfunctions=[]) %} {% for usecase in usecases %} {% if linked_needs(usecase["id"], "parent_needs_back")|list %}