From 17025ee9e09f04781e223e2ecd5448b9b29f05a2 Mon Sep 17 00:00:00 2001 From: Jochen Hoenle <173445474+hoe-jo@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:41:26 +0200 Subject: [PATCH] [rules score] add aou forwarding to lobster tracing --- bazel/rules/rules_score/BUILD | 1 + .../docs/requirements/tool_requirements.trlc | 28 ++- .../rules/rules_score/docs/rule_reference.rst | 8 + .../docs/user_guide/assumptions_of_use.rst | 51 +++++- .../rules_score/examples/integrator/BUILD | 12 +- .../requirements/component_requirements.trlc | 4 +- bazel/rules/rules_score/lobster/config/BUILD | 6 - .../lobster/config/lobster_comp_req.yaml | 1 + .../lobster/config/lobster_component.conf.tpl | 16 -- .../lobster/config/lobster_de.conf.tpl | 2 + bazel/rules/rules_score/private/component.bzl | 123 +++++-------- .../private/component_requirements.bzl | 4 +- .../private/dependable_element.bzl | 70 +++++++- .../rules_score/private/lobster_config.bzl | 25 ++- bazel/rules/rules_score/providers.bzl | 5 +- .../src/aou_forwarding_to_lobster.py | 168 +++++++++++++----- .../assets/component_diagram.puml | 2 +- bazel/rules/rules_score/test/BUILD | 13 +- .../rules_score/test/lobster_config_test.bzl | 57 ++++++ .../test/test_aou_forwarding_to_lobster.py | 131 +++++++++----- .../rules_score/test/unit_component_test.bzl | 15 +- .../trlc/config/score_requirements_model.rsl | 2 + 22 files changed, 520 insertions(+), 224 deletions(-) delete mode 100644 bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 8fa9a1b5..81a9bd92 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -110,6 +110,7 @@ py_binary( visibility = ["//visibility:public"], deps = [ requirement("pyyaml"), + "@lobster//lobster/common", ], ) diff --git a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc index a47952f4..9eb96733 100644 --- a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc +++ b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc @@ -130,7 +130,7 @@ section "Tool Requirements" { The dependable_element rule shall automatically include lobster traceability entries for all assumptions of use defined by its direct dependencies in the dependee's traceability report as - a "Forwarded AoUs" tier. + a "Received AoUs" tier. ''' mitigates = [AoU_Silently_Dropped] derived_from = [Forward_AoU_To_Dependees] @@ -162,12 +162,30 @@ section "Tool Requirements" { satisfied_by = Tools.Bazel } + ToolQualification.ToolRequirement Cover_Received_AoU_Via_Component_Requirement { + description = ''' + The CompReq requirement type shall support an optional + derived_from_aou field: a free-text list of AoU identifiers + (no cross-module TRLC reference) that this component + requirement implements. The dependable_element rule shall + convert this field to lobster trace references resolved + against the "Received AoUs" tier, so a component requirement + can cover a received AoU it handles locally without requiring + that AoU to be chain-forwarded. + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] + satisfied_by = Tools.Lobster + } + ToolQualification.ToolRequirement Include_Forwarded_AoUs_In_Traceability { description = ''' - The lobster traceability report of a dependee shall include - forwarded AoUs as traceable items so that the existing - lobster-ci-report test fails when forwarded AoUs are not - handled (linked to a requirement, test, or justification). + The lobster traceability report of a dependable element shall + require every item in its "Received AoUs" tier to be covered + by at least one of: a component requirement (via + derived_from_aou) or a chain-forwarding entry (via + aou_forwarding), so that the existing lobster-ci-report test + fails when a received AoU is neither handled nor forwarded. ''' mitigates = [AoU_Silently_Dropped] derived_from = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index c3d4fd44..bff7dcb5 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -274,6 +274,14 @@ testable within that component. **Generated targets:** ```` (documentation), ``_test`` (TRLC validation) +A ``CompReq`` record may also set ``derived_from_aou``, a free-text list of +identifiers (``PackageName.RecordName``, no ``@version``) naming AoUs +received by the enclosing ``dependable_element`` that this requirement +implements. Unlike ``derived_from``, this is a plain string field (no TRLC +cross-module reference), matched by name against received AoUs at the +``dependable_element`` lobster-report level — see +:doc:`user_guide/assumptions_of_use`. + .. _rule-assumptions-of-use: assumptions_of_use diff --git a/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst b/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst index f7da12c6..c3635483 100644 --- a/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst +++ b/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst @@ -21,7 +21,9 @@ or risk that is mitigated when this assumption is fulfilled. Traceability to requirements is established at the Bazel level via the ``deps`` attribute on the ``assumptions_of_use`` rule — there is no TRLC ``derived_from`` -or ``satisfies`` field on ``AoU``. +or ``satisfies`` field on ``AoU`` itself. A dependent component requirement can, +however, declare that it implements a received AoU via ``derived_from_aou`` +(see `AoU Forwarding`_ below). .. code-block:: text :caption: examples/seooc/docs/aous.trlc @@ -83,15 +85,45 @@ is forwarded rather than handled locally: calls to the library do not exceed the 10ms cycle time constraint imposed by the underlying other_seooc dependency. -**Handling forwarded AoUs in the dependee** -Forwarded AoUs appear as a "Forwarded AoUs" tier in the dependee's lobster -traceability report. The dependee must handle each forwarded AoU by one of: +**Handling AoUs received in the dependee** +Every AoU a dependable element receives (own AoUs forwarded automatically from +a dependency, plus anything that dependency itself chain-forwarded) appears as +an item in a "Received AoUs" tier in the dependee's lobster traceability +report. Each received AoU must be covered by exactly one of: -- Linking it to a component requirement that addresses the assumption -- Linking it to a test that verifies the assumption is met -- Chain-forwarding it further (with justification) to its own dependees +- **Handling it locally**: a component requirement's ``derived_from_aou`` field + names the AoU it implements (see below). This shows up as "Component + Requirements" coverage in the report. +- **Chain-forwarding it further** (with justification) via ``aou_forwarding``, + to be handled by this element's own dependees instead. This shows up as + "Forwarded AoUs" coverage in the report. -If a forwarded AoU is not handled, the ``bazel test`` traceability check will fail. +If a received AoU is neither handled nor forwarded, the ``bazel test`` +traceability check fails (in ``maturity = "release"``; in +``maturity = "development"`` it is only reported as a warning). + +**Handling a received AoU with a component requirement** +Add the AoU's identifier (``PackageName.RecordName``, matching the name used +in the upstream ``AoU`` TRLC record, without ``@version``) to the +``derived_from_aou`` field of the ``CompReq`` that implements it: + +.. code-block:: text + :caption: examples/integrator/docs/requirements/component_requirements.trlc + + ScoreReq.CompReq COMP_INT_001 { + description = "The startup module shall call the SEooC initialization routine before entering the main loop" + safety = ScoreReq.Asil.B + derived_from = [Integrator.FEAT_INT_001@1] + derived_from_aou = ["SampleType.SampleAoU"] + version = 1 + } + +``derived_from_aou`` is a plain free-text field (not a TRLC cross-module +reference), so no ``import`` of the upstream AoU's package is needed. It is +converted directly to a lobster trace reference and matched, by name, against +the AoUs this dependable element actually receives -- a typo or an AoU that is +not actually received will fail the build with an "unknown tracing target" +error rather than silently doing nothing. **Example: three-level forwarding chain** (the real working code for this example lives in ``examples/some_other_library``, ``examples/seooc``, and @@ -105,7 +137,8 @@ example lives in ``examples/some_other_library``, ``examples/seooc``, and → chain-forwards received TimingConstraint via aou_forwarding.yaml ↑ (deps) integrator_seooc → receives SampleType.SampleAoU (auto-forwarded) - and OtherLibrary.TimingConstraint (chain-forwarded), must handle both + and OtherLibrary.TimingConstraint (chain-forwarded) + → handles both locally via derived_from_aou (no further dependees) .. code-block:: starlark :caption: examples/seooc/BUILD diff --git a/bazel/rules/rules_score/examples/integrator/BUILD b/bazel/rules/rules_score/examples/integrator/BUILD index c963f8ad..ba8f613a 100644 --- a/bazel/rules/rules_score/examples/integrator/BUILD +++ b/bazel/rules/rules_score/examples/integrator/BUILD @@ -13,7 +13,8 @@ # # Example: System integrator SEooC that depends on safety_software_seooc_example. # -# This illustrates the full AoU forwarding chain: +# This illustrates the full AoU forwarding chain, and the two ways a +# dependable element can cover an AoU it receives: # # other_seooc defines AoU: OtherLibrary.TimingConstraint # ↑ (deps) @@ -22,9 +23,9 @@ # - chain-forwards OtherLibrary.TimingConstraint via aou_forwarding.yaml # ↑ (deps) # integrator_seooc (this target) -# - receives SampleType.SampleAoU (auto-forwarded from seooc) -# - receives OtherLibrary.TimingConstraint (chain-forwarded through seooc) -# - must handle both in its lobster traceability report +# - receives SampleType.SampleAoU (auto-forwarded from seooc) +# - receives OtherLibrary.TimingConstraint (chain-forwarded through seooc) +# - HANDLES both locally: COMP_INT_001 and COMP_INT_002 # load( @@ -71,6 +72,9 @@ component( # receives all forwarded AoUs: # - SampleType.SampleAoU (auto-forwarded, own AoU of seooc) # - OtherLibrary.TimingConstraint (chain-forwarded from other_seooc through seooc) +# Both are handled locally via `derived_from_aou` on component requirements +# (see docs/requirements/component_requirements.trlc); no further +# aou_forwarding.yaml is needed here. dependable_element( name = "integrator_seooc", diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc b/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc index b3becb90..ef976a20 100644 --- a/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc @@ -19,13 +19,15 @@ ScoreReq.CompReq COMP_INT_001 { description = "The startup module shall call the SEooC initialization routine before entering the main loop" safety = ScoreReq.Asil.B derived_from = [Integrator.FEAT_INT_001@1] + derived_from_aou = ["SampleType.SampleAoU"] version = 1 } ScoreReq.CompReq COMP_INT_002 { - description = "The cyclic task shall invoke the validation interface every 10ms using a hardware timer interrupt" + description = "The cyclic task shall invoke the validation interface every 10ms using a hardware timer interrupt, satisfying the underlying library's timing constraint" safety = ScoreReq.Asil.B derived_from = [Integrator.FEAT_INT_002@1] + derived_from_aou = ["OtherLibrary.TimingConstraint"] version = 1 } diff --git a/bazel/rules/rules_score/lobster/config/BUILD b/bazel/rules/rules_score/lobster/config/BUILD index 466b5ef0..837ab78a 100644 --- a/bazel/rules/rules_score/lobster/config/BUILD +++ b/bazel/rules/rules_score/lobster/config/BUILD @@ -55,12 +55,6 @@ filegroup( # for source file lists. Used by rules_score to generate per-target configs # via ctx.actions.expand_template() — only the source paths vary. # --------------------------------------------------------------------------- -filegroup( - name = "lobster_component_template", - srcs = ["lobster_component.conf.tpl"], - visibility = ["//visibility:public"], -) - filegroup( name = "lobster_de_template", srcs = ["lobster_de.conf.tpl"], diff --git a/bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml b/bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml index 3c1517f6..a720829d 100644 --- a/bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml +++ b/bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml @@ -31,3 +31,4 @@ conversion-rules: description-fields: description tags: - derived_from + - derived_from_aou diff --git a/bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl deleted file mode 100644 index c102809d..00000000 --- a/bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl +++ /dev/null @@ -1,16 +0,0 @@ -requirements "Feature Requirements" { -{FEAT_REQ_SOURCES} -} - -requirements "Component Requirements" { -{COMP_REQ_SOURCES} -} - -implementation "Architecture" { -{ARCH_SOURCES} - trace to: "Component Requirements"; -} - -activity "Unit Test" { -{UNIT_TEST_SOURCES} -} diff --git a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl index df39df77..57e56b26 100644 --- a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl +++ b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl @@ -1,5 +1,7 @@ {FEAT_REQ_BLOCK} +{RECEIVED_AOU_BLOCK} + {FORWARDED_AOU_BLOCK} {COMP_REQ_BLOCK} diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index 650e2dd1..7f70e1cc 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -19,9 +19,8 @@ following S-CORE process guidelines. A component consists of multiple units with associated requirements and tests. """ -load("@lobster//:lobster.bzl", "subrule_lobster_gtest", "subrule_lobster_html_report", "subrule_lobster_report") +load("@lobster//:lobster.bzl", "subrule_lobster_gtest") load("//bazel/rules/rules_score:providers.bzl", "AssumedSystemRequirementsInfo", "CertifiedScope", "ComponentInfo", "ComponentRequirementsInfo", "ComponentTestCaseCoverageInfo", "FeatureRequirementsInfo", "SphinxSourcesInfo", "UnitInfo") -load("//bazel/rules/rules_score/private:lobster_config.bzl", "format_lobster_sources") # ============================================================================ # Private Rule Implementation: Component .update target @@ -181,20 +180,34 @@ def _component_impl(ctx): # ------------------------------------------------------------------------- # Lobster Tracing: collect .lobster files from component_requirements targets - # and feature_requirements targets (needed to resolve derived_from references) + # and feature_requirements targets (needed to resolve derived_from references). + # Component requirement .lobster files always include `derived_from_aou` as + # a tracing target (resolved only at the dependable_element level). + # + # A feature_requirements/assumed_system_requirements target may also be + # listed in `requirements` purely so its rendered RST is available for + # derived_from cross-referencing at the TRLC level; its .lobster file is + # not otherwise used here (this component builds no report of its own — + # that only happens once, aggregated, at the dependable_element level). # ------------------------------------------------------------------------- req_lobster_files = [] - feat_req_lobster_files = [] + req_lobster_transitive_files = [] for req_target in ctx.attr.requirements: if ComponentRequirementsInfo in req_target: req_lobster_files.append(req_target[ComponentRequirementsInfo].srcs) - if FeatureRequirementsInfo in req_target: - feat_req_lobster_files.append(req_target[FeatureRequirementsInfo].srcs) - if AssumedSystemRequirementsInfo in req_target: - feat_req_lobster_files.append(req_target[AssumedSystemRequirementsInfo].srcs) + req_lobster_transitive_files.append(req_target[ComponentRequirementsInfo].srcs) + + # Bubble up requirement lobster files from nested components so + # dependable_element sees the full tree, not just this component's own + # requirements (ComponentInfo.requirements itself is intentionally scoped + # to only this component's own explicitly-listed requirements). + for component in ctx.attr.components: + if ComponentInfo in component: + if component[ComponentInfo].requirements_transitive: + req_lobster_transitive_files.append(component[ComponentInfo].requirements_transitive) req_lobster_depset = depset(transitive = req_lobster_files) - feat_req_lobster_depset = depset(transitive = feat_req_lobster_files) + req_lobster_transitive_depset = depset(transitive = req_lobster_transitive_files) # Collect nested components components_depset = depset(ctx.attr.components) @@ -256,57 +269,15 @@ def _component_impl(ctx): progress_message = "Generating architecture lobster for %s" % ctx.label, ) - # ------------------------------------------------------------------------- - # Generate Lobster Configuration: expand static template, substituting - # only the source file lists (the structure is fixed per variant). - # ------------------------------------------------------------------------- - comp_req_lobster_files = req_lobster_depset.to_list() - feat_req_lobster_files_list = feat_req_lobster_depset.to_list() - all_lobster_inputs = list(comp_req_lobster_files) + feat_req_lobster_files_list - - if arch_lobster_file: - all_lobster_inputs.append(arch_lobster_file) - - all_lobster_inputs.append(gtest_lobster_file) - - lobster_config = ctx.actions.declare_file(ctx.attr.name + "_traceability_config") - ctx.actions.expand_template( - template = ctx.file._lobster_comp_template, - output = lobster_config, - substitutions = { - "{FEAT_REQ_SOURCES}": format_lobster_sources(feat_req_lobster_files_list), - "{COMP_REQ_SOURCES}": format_lobster_sources(comp_req_lobster_files), - "{ARCH_SOURCES}": format_lobster_sources([arch_lobster_file] if arch_lobster_file else []), - "{UNIT_TEST_SOURCES}": format_lobster_sources([gtest_lobster_file]), - }, - ) - - # ------------------------------------------------------------------------- - # Lobster Report Build: produce .lobster report, HTML - # ------------------------------------------------------------------------- - lobster_report = subrule_lobster_report(all_lobster_inputs, lobster_config) - lobster_html_report = subrule_lobster_html_report(lobster_report) - - test_executable = ctx.actions.declare_file("{}_lobster_ci_test_executable".format(ctx.attr.name)) - ctx.actions.write( - output = test_executable, - content = "set -o pipefail; {} {}".format( - ctx.executable._lobster_ci_report.short_path, - lobster_report.short_path, - ), - ) - return [ - # DefaultInfo: lobster report as build output + CI test executable + # DefaultInfo: lobster traceability files this component produces directly + # (architecture + unit-test lobster). The full aggregated traceability + # report/CI check happens once, at the dependable_element level, which + # re-ingests these same files together with every other component in + # the tree — a component has no meaningful traceability report of its + # own to build or gate on. DefaultInfo( - runfiles = ctx.runfiles( - files = [ - ctx.executable._lobster_ci_report, - lobster_report, - ], - ).merge(ctx.attr._lobster_ci_report[DefaultInfo].default_runfiles), - files = depset([lobster_report, lobster_html_report]), - executable = test_executable, + files = depset(([arch_lobster_file] if arch_lobster_file else []) + [gtest_lobster_file]), ), # CertifiedScope: propagate certification scopes upward CertifiedScope(transitive_scopes = depset(transitive = collected_certified_scopes)), @@ -314,6 +285,7 @@ def _component_impl(ctx): ComponentInfo( name = ctx.label.name, requirements = req_lobster_depset, + requirements_transitive = req_lobster_transitive_depset, components = components_depset, tests = depset( [gtest_lobster_file], @@ -343,7 +315,7 @@ def _component_impl(ctx): # Rule Definition # ============================================================================ -_component_test = rule( +_component_rule = rule( implementation = _component_impl, doc = "Defines a software component composed of multiple units for S-CORE process compliance", attrs = { @@ -364,25 +336,14 @@ _component_test = rule( allow_single_file = True, doc = "Optional committed test_case_coverage.lock.yaml file for coverage validation", ), - "_lobster_ci_report": attr.label( - default = "@lobster//:lobster-ci-report", - executable = True, - cfg = "exec", - ), "_arch_to_reqs_from_lobster": attr.label( default = Label("//bazel/rules/rules_score:arch_to_reqs_from_lobster"), executable = True, cfg = "exec", doc = "Tool to extract component requirements and generate architecture .lobster items for component traceability", ), - "_lobster_comp_template": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:lobster_component_template"), - allow_single_file = True, - doc = "Lobster config template for component traceability.", - ), }, - subrules = [subrule_lobster_gtest, subrule_lobster_report, subrule_lobster_html_report], - test = True, + subrules = [subrule_lobster_gtest], ) # ============================================================================ @@ -406,16 +367,22 @@ def component( - Tests: Integration tests that verify the component as a whole - Coverage: Optional test case coverage traceability (if test_case_coverage_lock provided) + A component does not build or gate on a traceability report of its own — + its requirements/architecture/test lobster files are propagated up via + ComponentInfo and validated once, aggregated with every other component in + the tree, by the enclosing dependable_element's lobster-ci-report test. + Args: name: The name of the component. Used as the target name. requirements: List of labels to component_requirements targets that define the requirements for this component. A - feature_requirements target may also be listed here so its - .lobster file is available to resolve derived_from references - from the component requirements; only component_requirements - targets contribute rendered RST to the component's Sphinx docs, - so listing a feature_requirements target does not duplicate it - in the generated documentation. + feature_requirements target may also be listed here for + convenience/consistency; only component_requirements targets + contribute rendered RST to the component's Sphinx docs, so + listing a feature_requirements target does not duplicate it in + the generated documentation, and it has no effect on + traceability (derived_from resolution happens once, at the + dependable_element level). components: List of labels to nested component targets (for hierarchical component structures). tests: List of labels to Bazel test targets that verify the component @@ -440,7 +407,7 @@ def component( ``` """ - _component_test( + _component_rule( name = name, requirements = requirements, components = components, diff --git a/bazel/rules/rules_score/private/component_requirements.bzl b/bazel/rules/rules_score/private/component_requirements.bzl index 1ad91911..2bd4ab7f 100644 --- a/bazel/rules/rules_score/private/component_requirements.bzl +++ b/bazel/rules/rules_score/private/component_requirements.bzl @@ -55,7 +55,9 @@ def component_requirements( (``@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model``). Override this when using a custom requirements model. lobster_config: Optional Lobster extraction config label. Defaults to the - S-CORE component requirement config. + S-CORE component requirement config. Its conversion rules always + include `derived_from_aou` as a tracing target (resolved only at + the dependable_element level, which has a "Received AoUs" level). visibility: Bazel visibility specification for the generated targets. Generated Targets: diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index d806153d..40b16c79 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1018,15 +1018,18 @@ def _dependable_element_index_impl(ctx): feat_req_lobster_depset = depset(transitive = feat_req_lobster_files) - # Collect component requirement and test .lobster files from ComponentInfo + # Collect component requirement and test .lobster files from ComponentInfo. + # requirements_transitive rolls up every nested component's own + # requirements too (unlike ComponentInfo.requirements, which is scoped to + # a single component and used only by the test-case-coverage check below). comp_req_lobster_files = [] comp_test_lobster_files = [] comp_arch_lobster_files = [] for comp_target in ctx.attr.components: if ComponentInfo in comp_target: comp_info = comp_target[ComponentInfo] - if comp_info.requirements: - comp_req_lobster_files.append(comp_info.requirements) + if comp_info.requirements_transitive: + comp_req_lobster_files.append(comp_info.requirements_transitive) if comp_info.tests: comp_test_lobster_files.append(comp_info.tests) if comp_info.architecture: @@ -1119,6 +1122,11 @@ def _dependable_element_index_impl(ctx): # Build the DE-level lobster report if feature and component traces exist feat_req_list = feat_req_lobster_depset.to_list() + + # Component requirement .lobster files always include `derived_from_aou` + # refs (so a component requirement can cover a received AoU); this is the + # only lobster-report/CI-test built for these files — component() builds + # no report of its own. comp_req_list = comp_req_lobster_depset.to_list() comp_test_list = comp_test_lobster_depset.to_list() comp_arch_list = comp_arch_lobster_depset.to_list() @@ -1155,27 +1163,39 @@ def _dependable_element_index_impl(ctx): received_aou_lobster_depset = depset(transitive = received_aou_lobster_files) received_aou_list = received_aou_lobster_depset.to_list() - # Chain-forwarding: if aou_forwarding YAML is provided, filter received AoUs + # Chain-forwarding: if aou_forwarding YAML is provided, filter received AoUs. + # Also produces a "markers" file: synthetic items (distinct tags, `refs` + # pointing at the original received AoU tags) used only in THIS element's + # own report as the "Forwarded AoUs" level -- the identity-preserved + # chain_forwarded_lobster_file above cannot be reused there because it would + # collide (same tag) with the "Received AoUs" level in the same report. chain_forwarded_lobster_depset = depset() + forwarded_aou_markers_list = [] if ctx.file.aou_forwarding and received_aou_list: chain_forwarded_lobster_file = ctx.actions.declare_file( ctx.label.name + "/chain_forwarded_aous.lobster", ) + forwarded_aou_markers_file = ctx.actions.declare_file( + ctx.label.name + "/forwarded_aou_markers.lobster", + ) fwd_args = ctx.actions.args() fwd_args.add("--yaml", ctx.file.aou_forwarding) fwd_args.add("--output", chain_forwarded_lobster_file) + fwd_args.add("--markers-output", forwarded_aou_markers_file) fwd_args.add("--input-lobster") fwd_args.add_all(received_aou_list) ctx.actions.run( inputs = [ctx.file.aou_forwarding] + received_aou_list, - outputs = [chain_forwarded_lobster_file], + outputs = [chain_forwarded_lobster_file, forwarded_aou_markers_file], executable = ctx.executable._aou_forwarding_tool, arguments = [fwd_args], progress_message = "Filtering chain-forwarded AoUs for %s" % ctx.label.name, mnemonic = "AoUForwarding", ) chain_forwarded_lobster_depset = depset([chain_forwarded_lobster_file]) + forwarded_aou_markers_list = [forwarded_aou_markers_file] output_files.append(chain_forwarded_lobster_file) + output_files.append(forwarded_aou_markers_file) lobster_report_file = None lobster_html_report = None @@ -1231,6 +1251,7 @@ def _dependable_element_index_impl(ctx): has_feat_req = bool(feat_req_list) has_comp_req = bool(comp_req_list) + has_received_aou = bool(received_aou_list) has_public_api = bool(interface_req_list) or strict has_fm = bool(fm_list) or strict has_cm = bool(cm_list) or strict @@ -1242,12 +1263,45 @@ def _dependable_element_index_impl(ctx): output = lobster_config, substitutions = { "{FEAT_REQ_BLOCK}": format_lobster_block("requirements", "Feature Requirements", feat_req_list), - "{FORWARDED_AOU_BLOCK}": format_lobster_block("requirements", "Forwarded AoUs", received_aou_list), + # Target level: every item here must be covered, either by a + # Component Requirement (`derived_from_aou`) or by being + # further chain-forwarded (Forwarded AoUs, below). Without the + # `requires` override, LOBSTER would instead require BOTH + # sources to independently cover every item (its default + # AND-across-sources behaviour for multiple `trace to:` + # declarations), which no single AoU could ever satisfy. + "{RECEIVED_AOU_BLOCK}": format_lobster_block( + "requirements", + "Received AoUs", + received_aou_list, + requires = [ + ([ + "Component Requirements", + ] if has_comp_req else []) + ([ + "Forwarded AoUs", + ] if forwarded_aou_markers_list or strict else []), + ], + ), + # Checking level: markers for received AoUs this element chain- + # forwards onward instead of handling locally. Force-emitted + # empty in release mode (when there are received AoUs) so the + # "trace to: Received AoUs" edge stays active even without an + # aou_forwarding.yaml -- otherwise an element that receives + # AoUs but neither forwards nor handles any of them would have + # no checking level at all, and the missing coverage would + # silently pass instead of failing the build. + "{FORWARDED_AOU_BLOCK}": format_lobster_block( + "requirements", + "Forwarded AoUs", + forwarded_aou_markers_list, + trace_to = ["Received AoUs"] if has_received_aou else [], + emit_empty = strict and has_received_aou, + ), "{COMP_REQ_BLOCK}": format_lobster_block( "requirements", "Component Requirements", comp_req_list, - trace_to = ["Feature Requirements"] if has_feat_req else [], + trace_to = (["Feature Requirements"] if has_feat_req else []) + (["Received AoUs"] if has_received_aou else []), ), "{UNIT_TEST_BLOCK}": format_lobster_block( "activity", @@ -1299,7 +1353,7 @@ def _dependable_element_index_impl(ctx): }, ) - all_lobster_inputs = feat_req_list + comp_req_list + comp_arch_list + comp_test_list + interface_req_list + fm_list + cm_list + rc_list + received_aou_list + coverage_lobster_files + all_lobster_inputs = feat_req_list + comp_req_list + comp_arch_list + comp_test_list + interface_req_list + fm_list + cm_list + rc_list + received_aou_list + forwarded_aou_markers_list + coverage_lobster_files lobster_report_file = subrule_lobster_report(all_lobster_inputs, lobster_config) lobster_files = [lobster_config, lobster_report_file] diff --git a/bazel/rules/rules_score/private/lobster_config.bzl b/bazel/rules/rules_score/private/lobster_config.bzl index d201e084..7e8188fb 100644 --- a/bazel/rules/rules_score/private/lobster_config.bzl +++ b/bazel/rules/rules_score/private/lobster_config.bzl @@ -32,7 +32,7 @@ def format_lobster_sources(files): """ return "\n".join([' source: "{}";'.format(f.path) for f in files]) -def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): +def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False, requires = []): """Build a complete, optional LOBSTER config block. By default returns "" (omitting the level entirely) when `files` is empty, @@ -48,6 +48,17 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): a missing verification level (e.g. no unit tests, no root causes) fail the traceability check instead of silently disappearing from the policy. + By default, when *multiple* levels each declare `trace to: `, + LOBSTER requires ALL of them to independently cover every item here (an + AND across sources) -- see the `requires` config directive in LOBSTER's + documentation. Pass `requires` to relax this to an OR: each element is a + list of level names, rendered as a single `requires: "A" or "B";` line + (multiple elements become separate, independently-AND'd `requires:` + lines). Only include level names that are themselves present (non-empty + or emit_empty) in the same config, and that already declare a `trace to:` + line pointing at this level (`requires` narrows an existing mandatory + `trace to:` edge into an alternative, it does not create one). + Args: kind: LOBSTER block kind, e.g. "requirements", "activity", "implementation". name: Level name, used as the quoted block header. @@ -60,6 +71,10 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): emit_empty: When True, emit the block header and `trace to:` lines even if `files` is empty (no `source:` lines are produced). Defaults to False. + requires: List of OR-groups (each a list of level names) that jointly + replace the default AND-of-sources coverage check for this level + with "any one of these is sufficient". Defaults to [] (no + override; the default AND-of-sources behaviour applies). Returns: The full block text (kind, header, sources, trace-to lines), or "" @@ -68,9 +83,15 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): if not files and not emit_empty: return "" trace_lines = "".join([' trace to: "{}";\n'.format(t) for t in trace_to]) - return '{kind} "{name}" {{\n{sources}\n{trace}}}'.format( + requires_lines = "".join([ + " requires: {};\n".format(" or ".join(['"{}"'.format(n) for n in group])) + for group in requires + if group + ]) + return '{kind} "{name}" {{\n{sources}\n{trace}{requires}}}'.format( kind = kind, name = name, sources = format_lobster_sources(files), trace = trace_lines, + requires = requires_lines, ) diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index d9d4554b..19e48161 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -98,7 +98,7 @@ FeatureRequirementsInfo = provider( ComponentRequirementsInfo = provider( doc = "Provider for component requirements artifacts.", fields = { - "srcs": "Depset of .lobster traceability files generated from TRLC requirement sources.", + "srcs": "Depset of .lobster traceability files generated from TRLC requirement sources. Includes `derived_from_aou` as a tracing target .", "name": "Name of the requirements target.", }, ) @@ -145,7 +145,8 @@ ComponentInfo = provider( doc = "Provider for component artifacts.", fields = { "name": "Name of the component target.", - "requirements": "Depset of component requirement traceability files (.lobster) collected from ComponentRequirementsInfo targets only (CompReq kind). Does not include feature or assumed-system requirement files.", + "requirements": "Depset of component requirement traceability files (.lobster) collected from this component's own ComponentRequirementsInfo targets only (CompReq kind); not rolled up from nested components. Does not include feature or assumed-system requirement files. Consumed only by dependable_element's test-case-coverage-lock check. Includes `derived_from_aou` tracing targets (resolved only at the dependable_element level).", + "requirements_transitive": "Depset of component requirement traceability files (.lobster), rolled up transitively from this component and all nested components. Consumed by dependable_element to build the aggregated 'Component Requirements' traceability report tier.", "components": "Depset of nested component and/or unit Targets that comprise this component.", "tests": "Depset of test traceability files (.lobster) generated from unit test results, collected transitively from all nested components and units.", "architecture": "Depset of architecture traceability files (.lobster) generated from unit architectural designs, collected transitively from all nested components and units.", diff --git a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py index 5d19702a..dd55f147 100644 --- a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py +++ b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py @@ -16,15 +16,32 @@ files, then outputs a new .lobster file containing only the entries listed in the YAML. This enables dependable elements to further-forward AoUs they cannot handle to their own dependees. + +Optionally also emits a second "markers" .lobster file: one synthetic item +per forwarded entry, distinct from (but referencing) the original received +AoU item. This is what lets the dependable_element's own traceability report +show a "Forwarded AoUs" level with a `trace to: "Received AoUs"` edge — +using the original (identity-preserved) items directly would create a tag +collision with the "Received AoUs" level in the same report. + +Reuses ``Requirement``, ``Tracing_Tag``, ``File_Reference``, ``lobster_read``, +and ``lobster_write`` from the lobster library (no manual JSON construction +or envelope/schema handling) — only the YAML parsing and the AoU-ID-to-tag +matching (which lobster itself has no concept of) are specific to this tool. """ from __future__ import annotations import argparse -import json from pathlib import Path import yaml +from lobster.common.errors import LOBSTER_Error, Message_Handler +from lobster.common.io import lobster_read, lobster_write +from lobster.common.items import Requirement, Tracing_Tag +from lobster.common.location import File_Reference + +GENERATOR = "aou_forwarding_to_lobster" def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: @@ -75,31 +92,35 @@ def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: return result -def load_lobster_items(lobster_paths: list[str]) -> list[dict]: - """Load all items from one or more .lobster JSON files. +def load_lobster_items(lobster_paths: list[str]) -> list[Requirement]: + """Load all Requirement items from one or more .lobster JSON files. Args: lobster_paths: Paths to .lobster files. Returns: - List of all lobster item dicts from all files. + List of all Requirement items from all files. + + Raises: + SystemExit: If a file cannot be read, or is not valid lobster-req-trace JSON. """ - all_items = [] + mh = Message_Handler() + all_items: list[Requirement] = [] for path in lobster_paths: + items: dict = {} try: - with open(path, encoding="utf-8") as f: - data = json.load(f) - except (json.JSONDecodeError, OSError) as e: + lobster_read(mh, path, "aou", items) + except (OSError, LOBSTER_Error) as e: raise SystemExit(f"Failed to parse lobster file {path}: {e}") from e - all_items.extend(data.get("data", [])) + all_items.extend(items.values()) return all_items -def filter_forwarded_aous( +def _match_forwarded_entries( forwarding_entries: list[dict[str, str]], - lobster_items: list[dict], -) -> list[dict]: - """Filter lobster items to only those listed in the forwarding YAML. + lobster_items: list[Requirement], +) -> list[tuple[dict[str, str], Requirement]]: + """Match each forwarding YAML entry to its received AoU lobster item. Matches by checking if the AoU ID appears in the lobster item's tag. Lobster-trlc generates tags like "req PackageName.RecordName@version". @@ -111,7 +132,7 @@ def filter_forwarded_aous( lobster_items: All lobster items from received AoU files. Returns: - Filtered list of lobster items matching the forwarding entries. + List of (entry, matched item) pairs, in forwarding YAML order. Raises: SystemExit: If any aou_id from YAML doesn't match a received item. @@ -119,20 +140,16 @@ def filter_forwarded_aous( # Build lookup: tag suffix -> item # Lobster-trlc may generate versioned tags like "req Pkg.Name@1". # We index by both the full ID and the base ID (without @version). - item_by_id: dict[str, dict] = {} + item_by_id: dict[str, Requirement] = {} for item in lobster_items: - tag = item.get("tag", "") - # Tags are formatted as "req PackageName.RecordName[@version]" - parts = tag.split(" ", 1) - if len(parts) == 2: - full_id = parts[1] - item_by_id[full_id] = item - # Also index by base name (strip @version suffix) - base_id = full_id.split("@")[0] - if base_id != full_id: - item_by_id[base_id] = item - - filtered = [] + full_id = item.tag.tag + if item.tag.version: + full_id += f"@{item.tag.version}" + item_by_id[full_id] = item + # Also index by base name (strip @version suffix) + item_by_id[item.tag.tag] = item + + matched = [] for entry in forwarding_entries: aou_id = entry["aou_id"] if aou_id not in item_by_id: @@ -141,19 +158,76 @@ def filter_forwarded_aous( f"AoU ID '{aou_id}' listed in forwarding YAML not found in received AoUs. " f"Available IDs: {available}" ) - filtered.append(item_by_id[aou_id]) + matched.append((entry, item_by_id[aou_id])) + + return matched + + +def filter_forwarded_aous( + forwarding_entries: list[dict[str, str]], + lobster_items: list[Requirement], +) -> list[Requirement]: + """Filter lobster items to only those listed in the forwarding YAML. + + The returned items are identical (same tag) to the originals: this + output is handed on, unmodified, to this element's own dependees so + that further chain-forwarding and eventual handling still resolves + against the AoU's original tag. + + Args: + forwarding_entries: Parsed YAML entries with 'aou_id' fields. + lobster_items: All lobster items from received AoU files. + + Returns: + Filtered list of lobster items matching the forwarding entries. + + Raises: + SystemExit: If any aou_id from YAML doesn't match a received item. + """ + return [item for _, item in _match_forwarded_entries(forwarding_entries, lobster_items)] - return filtered +def build_forwarded_markers( + forwarding_entries: list[dict[str, str]], + lobster_items: list[Requirement], + yaml_path: str, +) -> list[Requirement]: + """Build synthetic "Forwarded AoUs" marker items for the DE's own report. + + Each marker is a distinct lobster item (its own tag, so it does not + collide with the "Received AoUs" level in the same report) carrying a + `refs` entry pointing at the original received AoU tag. This gives + LOBSTER a `trace to: "Received AoUs"` edge for AoUs that are being + chain-forwarded rather than handled locally. The forwarding + justification becomes the marker's descriptive text. -def create_lobster_output(items: list[dict]) -> dict: - """Wrap items in the standard lobster JSON envelope.""" - return { - "schema": "lobster-req-trace", - "version": 3, - "generator": "aou_forwarding_to_lobster", - "data": items, - } + Args: + forwarding_entries: Parsed YAML entries with 'aou_id' and + 'justification' fields. + lobster_items: All lobster items from received AoU files. + yaml_path: Path to the aou_forwarding.yaml file (used as the + marker's source location). + + Returns: + List of marker Requirement items, one per forwarding entry. + + Raises: + SystemExit: If any aou_id from YAML doesn't match a received item. + """ + markers = [] + for entry, item in _match_forwarded_entries(forwarding_entries, lobster_items): + aou_id = entry["aou_id"] + marker = Requirement( + tag=Tracing_Tag("req", f"{aou_id}__forwarded"), + location=File_Reference(yaml_path, line=1), + framework="AoUForwarding", + kind="ForwardedAoU", + name=aou_id, + text=entry["justification"], + ) + marker.add_tracing_target(item.tag) + markers.append(marker) + return markers def main() -> None: @@ -177,6 +251,13 @@ def main() -> None: required=True, help="Output .lobster file path for the filtered entries.", ) + parser.add_argument( + "--markers-output", + required=False, + help="Optional output .lobster file path for synthetic 'Forwarded AoUs' " + "marker items (distinct tags, refs pointing at the original received " + "AoU items). Used by the dependable_element's own traceability report.", + ) args = parser.parse_args() @@ -186,16 +267,21 @@ def main() -> None: # Load received lobster items lobster_items = load_lobster_items(args.input_lobster) - # Filter + # Filter (identity-preserved copies, forwarded on to this element's own dependees) filtered_items = filter_forwarded_aous(forwarding_entries, lobster_items) # Write output - output = create_lobster_output(filtered_items) output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: - json.dump(output, f, indent=2) - f.write("\n") + lobster_write(f, Requirement, GENERATOR, filtered_items) + + if args.markers_output: + markers = build_forwarded_markers(forwarding_entries, lobster_items, args.yaml) + markers_output_path = Path(args.markers_output) + markers_output_path.parent.mkdir(parents=True, exist_ok=True) + with open(markers_output_path, "w", encoding="utf-8") as f: + lobster_write(f, Requirement, GENERATOR, markers) if __name__ == "__main__": diff --git a/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml b/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml index 0bb54fbe..ee2dc8fa 100644 --- a/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml +++ b/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml @@ -24,7 +24,7 @@ package "Inputs" { package "component rule\n(component.bzl)" { [subrule_lobster_gtest] as subrule - [_component_test\n(test_case_coverage_lock attr)] as rule_test + [_component_rule\n(test_case_coverage_lock attr)] as rule_test [_component_update\n(.update target)] as rule_update } diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 80cff397..0cfc3dd1 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -596,6 +596,14 @@ dependable_element( components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", + # This fixture is reused by several unrelated tests (Sphinx module + # generation, needs providers, index generation, ...) and reuses the same + # `:aous` fixture as its dependency `dep_seooc_lib`, so the AoUs it + # receives are neither locally handled (no components/derived_from_aou) + # nor chain-forwarded (no aou_forwarding.yaml). "development" keeps the + # AoU traceability check advisory here; see the dedicated AoU coverage + # tests for release-mode enforcement. + maturity = "development", requirements = [":feat_req"], tests = [], deps = [":dep_seooc_lib"], # dependency to other seoocs/score_components @@ -1337,7 +1345,10 @@ py_test( name = "test_aou_forwarding_to_lobster", size = "small", srcs = ["test_aou_forwarding_to_lobster.py"], - deps = ["@score_tooling//bazel/rules/rules_score:aou_forwarding_to_lobster"], + deps = [ + "@lobster//lobster/common", + "@score_tooling//bazel/rules/rules_score:aou_forwarding_to_lobster", + ], ) py_test( diff --git a/bazel/rules/rules_score/test/lobster_config_test.bzl b/bazel/rules/rules_score/test/lobster_config_test.bzl index c9cae8be..8a2ae1d3 100644 --- a/bazel/rules/rules_score/test/lobster_config_test.bzl +++ b/bazel/rules/rules_score/test/lobster_config_test.bzl @@ -150,3 +150,60 @@ def lobster_config_test_suite(name): trace_to = ["Failure Modes", "Control Measures"], ), ) + + # --- format_lobster_block: requires (OR-of-sources override) -------------- + # By default, when multiple different levels each `trace to:` the same + # target, LOBSTER requires ALL of them to independently cover every item + # (AND across sources). `requires` overrides this to "any one of these is + # sufficient" for a given target level -- see e.g. Received AoUs, which + # must be covered by EITHER Component Requirements OR Forwarded AoUs, not + # both at once. + + loadingtest.equals( + env, + "block_with_requires_or_group", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n requires: "Component Requirements" or "Forwarded AoUs";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [["Component Requirements", "Forwarded AoUs"]], + ), + ) + + loadingtest.equals( + env, + "block_with_requires_single_name", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n requires: "Component Requirements";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [["Component Requirements"]], + ), + ) + + loadingtest.equals( + env, + "block_with_requires_empty_group_omitted", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [[]], + ), + ) + + loadingtest.equals( + env, + "block_with_trace_to_and_requires_combined", + 'requirements "Component Requirements" {\n source: "comp.lobster";\n trace to: "Feature Requirements";\n requires: "Feature Requirements" or "Received AoUs";\n}', + format_lobster_block( + "requirements", + "Component Requirements", + [_fake_file("comp.lobster")], + trace_to = ["Feature Requirements"], + requires = [["Feature Requirements", "Received AoUs"]], + ), + ) diff --git a/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py b/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py index 49daead2..9b8f0178 100644 --- a/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py +++ b/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py @@ -12,20 +12,33 @@ # ******************************************************************************* """Tests for aou_forwarding_to_lobster.""" -import json import tempfile import unittest import yaml +from lobster.common.items import Requirement, Tracing_Tag +from lobster.common.location import Void_Reference from aou_forwarding_to_lobster import ( - create_lobster_output, + build_forwarded_markers, filter_forwarded_aous, load_lobster_items, parse_forwarding_yaml, ) +def _req(tag: str, name: str) -> Requirement: + """Build a minimal Requirement item with the given 'req Pkg.Name[@ver]' tag.""" + namespace, rest = tag.split(" ", 1) + return Requirement( + tag=Tracing_Tag.from_text(namespace, rest), + location=Void_Reference(), + framework="TRLC", + kind="AoU", + name=name, + ) + + class TestParseForwardingYaml(unittest.TestCase): """Tests for parse_forwarding_yaml.""" @@ -79,31 +92,24 @@ def test_multiple_entries(self) -> None: class TestLoadLobsterItems(unittest.TestCase): """Tests for load_lobster_items.""" - def _write_lobster(self, items: list) -> str: - data = { - "schema": "lobster-req-trace", - "version": 3, - "generator": "test", - "data": items, - } + def _write_lobster(self, tags: list[str]) -> str: + from lobster.common.io import lobster_write + + items = [_req(tag, tag.split(" ", 1)[1].split("@")[0]) for tag in tags] f = tempfile.NamedTemporaryFile(mode="w", suffix=".lobster", delete=False) - json.dump(data, f) + lobster_write(f, Requirement, "test", items) f.close() return f.name def test_loads_items(self) -> None: - items = [ - {"tag": "req Pkg.AoU1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2", "name": "AoU2"}, - ] - path = self._write_lobster(items) + path = self._write_lobster(["req Pkg.AoU1", "req Pkg.AoU2"]) loaded = load_lobster_items([path]) self.assertEqual(len(loaded), 2) - self.assertEqual(loaded[0]["tag"], "req Pkg.AoU1") + self.assertEqual(str(loaded[0].tag), "req Pkg.AoU1") def test_multiple_files(self) -> None: - path1 = self._write_lobster([{"tag": "req A.B", "name": "B"}]) - path2 = self._write_lobster([{"tag": "req C.D", "name": "D"}]) + path1 = self._write_lobster(["req A.B"]) + path2 = self._write_lobster(["req C.D"]) loaded = load_lobster_items([path1, path2]) self.assertEqual(len(loaded), 2) @@ -117,20 +123,17 @@ class TestFilterForwardedAous(unittest.TestCase): """Tests for filter_forwarded_aous.""" def test_filters_correctly(self) -> None: - items = [ - {"tag": "req Pkg.AoU1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2", "name": "AoU2"}, - ] + items = [_req("req Pkg.AoU1", "AoU1"), _req("req Pkg.AoU2", "AoU2")] entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) - self.assertEqual(filtered[0]["tag"], "req Pkg.AoU1") + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1") def test_multiple_filters(self) -> None: items = [ - {"tag": "req A.B", "name": "B"}, - {"tag": "req C.D", "name": "D"}, - {"tag": "req E.F", "name": "F"}, + _req("req A.B", "B"), + _req("req C.D", "D"), + _req("req E.F", "F"), ] entries = [ {"aou_id": "A.B", "justification": "r1"}, @@ -140,7 +143,7 @@ def test_multiple_filters(self) -> None: self.assertEqual(len(filtered), 2) def test_nonexistent_aou_id_raises(self) -> None: - items = [{"tag": "req Pkg.AoU1", "name": "AoU1"}] + items = [_req("req Pkg.AoU1", "AoU1")] entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] with self.assertRaises(SystemExit): filter_forwarded_aous(entries, items) @@ -148,36 +151,78 @@ def test_nonexistent_aou_id_raises(self) -> None: def test_versioned_tag_matches_base_id(self) -> None: """lobster-trlc generates versioned tags like 'req Pkg.Name@1'.""" items = [ - {"tag": "req Pkg.AoU1@1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2@3", "name": "AoU2"}, + _req("req Pkg.AoU1@1", "AoU1"), + _req("req Pkg.AoU2@3", "AoU2"), ] entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) - self.assertEqual(filtered[0]["tag"], "req Pkg.AoU1@1") + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1@1") def test_versioned_tag_matches_full_id(self) -> None: """Full versioned ID should also work.""" - items = [{"tag": "req Pkg.AoU1@2", "name": "AoU1"}] + items = [_req("req Pkg.AoU1@2", "AoU1")] entries = [{"aou_id": "Pkg.AoU1@2", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) -class TestCreateLobsterOutput(unittest.TestCase): - """Tests for create_lobster_output.""" +class TestBuildForwardedMarkers(unittest.TestCase): + """Tests for build_forwarded_markers.""" + + def test_builds_one_marker_per_entry(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1"), _req("req Pkg.AoU2@1", "AoU2")] + entries = [ + {"aou_id": "Pkg.AoU1", "justification": "reason 1"}, + {"aou_id": "Pkg.AoU2", "justification": "reason 2"}, + ] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(len(markers), 2) + + def test_marker_has_distinct_tag_and_refs_original(self) -> None: + """The marker's tag must not collide with the original item's tag + (so it can coexist with the "Received AoUs" level in the same + report), but its refs must point at the original tag.""" + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + marker = markers[0] + self.assertNotEqual(str(marker.tag), "req Pkg.AoU1@1") + self.assertEqual(str(marker.tag), "req Pkg.AoU1__forwarded") + self.assertEqual( + [str(ref) for ref in marker.unresolved_references], + ["req Pkg.AoU1@1"], + ) + + def test_marker_uses_justification_as_text(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "must be handled downstream"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(markers[0].text, "must be handled downstream") + + def test_marker_kind_and_framework(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(markers[0].kind, "ForwardedAoU") + self.assertEqual(markers[0].framework, "AoUForwarding") + + def test_marker_location_uses_yaml_path(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "some/path/aou_forwarding.yaml") + self.assertEqual(markers[0].location.filename, "some/path/aou_forwarding.yaml") - def test_wraps_items(self) -> None: - items = [{"tag": "req Foo.Bar", "name": "Bar"}] - output = create_lobster_output(items) - self.assertEqual(output["schema"], "lobster-req-trace") - self.assertEqual(output["version"], 3) - self.assertEqual(output["generator"], "aou_forwarding_to_lobster") - self.assertEqual(output["data"], items) + def test_nonexistent_aou_id_raises(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] + with self.assertRaises(SystemExit): + build_forwarded_markers(entries, items, "aou_forwarding.yaml") - def test_empty_items(self) -> None: - output = create_lobster_output([]) - self.assertEqual(output["data"], []) + def test_empty_entries_produces_no_markers(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + markers = build_forwarded_markers([], items, "aou_forwarding.yaml") + self.assertEqual(markers, []) if __name__ == "__main__": diff --git a/bazel/rules/rules_score/test/unit_component_test.bzl b/bazel/rules/rules_score/test/unit_component_test.bzl index 7ad2b46a..fbf5b79f 100644 --- a/bazel/rules/rules_score/test/unit_component_test.bzl +++ b/bazel/rules/rules_score/test/unit_component_test.bzl @@ -194,20 +194,23 @@ def _component_test_case_coverage_lock_test_impl(ctx): "Component with test_case_coverage_lock should provide ComponentInfo", ) - # DefaultInfo.files must contain the lobster report (non-empty). + # DefaultInfo.files must contain this component's own architecture/unit-test + # lobster files (non-empty). The component no longer builds an aggregated + # traceability report of its own — that happens once, at the + # dependable_element level. output_files = target_under_test[DefaultInfo].files.to_list() asserts.true( env, len(output_files) > 0, - "Component with test_case_coverage_lock should declare output files (expected lobster report); got: %s" % output_files, + "Component with test_case_coverage_lock should declare output files (expected .lobster files); got: %s" % output_files, ) - # At least one output file should be named like the lobster report. - lobster_report_present = any([f.basename.endswith(".lobster_report") or f.extension == "html" or f.basename.endswith(".lobster") for f in output_files]) + # At least one output file should be a .lobster traceability file. + lobster_file_present = any([f.basename.endswith(".lobster") for f in output_files]) asserts.true( env, - lobster_report_present, - "Component with test_case_coverage_lock should declare a lobster report output; got: %s" % [f.basename for f in output_files], + lobster_file_present, + "Component with test_case_coverage_lock should declare a .lobster output; got: %s" % [f.basename for f in output_files], ) return analysistest.end(env) diff --git a/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl b/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl index 3a2d44ce..9674e0d2 100644 --- a/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl +++ b/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl @@ -79,6 +79,8 @@ tuple CompReqSourceId { type CompReq "Component-level requirement allocated to a specific software component." extends RequirementSafety { derived_from "Versioned references to the FeatReq or AssumedSystemReq items this component requirement is derived from. Omit only for component-internal requirements with no feature-level parent." CompReqSourceId[1 .. *] + derived_from_aou "Free-text identifiers of received Assumptions of Use (e.g. \"OtherLibrary.TimingConstraint\") that this component requirement handles. Used at the dependable_element level to trace received AoUs to the component requirement that implements them. Plain strings (no cross-module TRLC reference), matched against the received AoU lobster item names." + optional String[0 .. *] } tuple CompReqId {