diff --git a/MODULE.bazel b/MODULE.bazel index 04c6fd1e..caa9fae2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -39,14 +39,7 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "flatbuffers", version = "25.9.23") bazel_dep(name = "download_utils", version = "1.2.2") bazel_dep(name = "trlc", version = "3.0.0") -bazel_dep(name = "lobster", version = "1.0.3") -git_override( - module_name = "lobster", - # Bumped for req_coverage's use of lobster.common.{io,items,errors,location}. - commit = "247927a438647f68c70a61f9fdde155f5aee145d", - remote = "https://github.com/bmw-software-engineering/lobster.git", -) - +bazel_dep(name = "lobster", version = "1.0.4") bazel_dep(name = "rules_distroless", version = "0.8.0") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") diff --git a/bazel/rules/rules_score/README.md b/bazel/rules/rules_score/README.md index 03d72afe..36b58507 100644 --- a/bazel/rules/rules_score/README.md +++ b/bazel/rules/rules_score/README.md @@ -254,10 +254,13 @@ Internally: artifacts, runs validation/core architecture checks as a subrule, and produces a DE-level LOBSTER report (`lobster_de.conf` template covering Feature Req → Component Req → Architecture → Public API → Failure Modes). + 2. `sphinx_module` compiles all RST sources + diagrams into an HTML zip. **`bazel test`** — runs the LOBSTER CI report embedded in the index rule -and all component / unit tests transitively. +and all component / unit tests transitively. With `maturity = "development"` +a failing traceability check only prints a warning instead of failing the +test. ```bash bazel build //examples/seooc:safety_software_seooc_example # HTML zip diff --git a/bazel/rules/rules_score/docs/_assets/lobster_report_assembly.puml b/bazel/rules/rules_score/docs/_assets/lobster_report_assembly.puml new file mode 100644 index 00000000..55f1137e --- /dev/null +++ b/bazel/rules/rules_score/docs/_assets/lobster_report_assembly.puml @@ -0,0 +1,75 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml lobster_report_assembly + +' How the dependable_element _index rule assembles the lobster traceability +' report and splices it into the Sphinx documentation tree (stages A-G). + +skinparam linetype ortho +skinparam ArrowFontSize 10 +skinparam defaultTextAlignment center + +skinparam rectangle { + BackgroundColor<> #EFF6FB + BorderColor<> #0066B1 + BackgroundColor<> #E3F2FD + BorderColor<> #1565C0 + BackgroundColor<> #E8F5E9 + BorderColor<> #2E7D32 + BackgroundColor<> #FFF3E0 + BorderColor<> #EF6C00 + BackgroundColor<> #F3E5F5 + BorderColor<> #6A1B9A +} + +rectangle "A: Per-level .lobster files\n(Feature/Component Req, Unit Test,\nCoverage, Architecture, Public API,\nFailure Modes, Control Measures,\nRoot Causes)" <> as A + +rectangle "B: expand_template\nformat_lobster_block() per level\n(emit_empty=strict in release)" <> as B +rectangle "de_traceability_config" <> as conf + +rectangle "C: subrule_lobster_report\nlobster-report (lobster-config flag)" <> as C +rectangle "report.json" <> as report + +rectangle "D: subrule_lobster_html_report\n(optional, generate_html_report)" <> as D +rectangle "report.html" <> as html + +rectangle "E: lobster-rst-report\n(out-dir, source-root flags)" <> as E +rectangle "traceability_report/\n(index.rst + one page/level)" <> as rst + +rectangle "F: _section_page +\nexpand_template(index.rst)\n(toctree entry added iff E ran)" <> as F +rectangle "_index/index.rst" <> as index + +rectangle "G: DefaultInfo +\nDependableElementLobsterInfo +\nOutputGroupInfo(lobster_ci_report)" <> as G + +rectangle " test\nlobster-ci-report (pass/fail gate)" <> as gate +rectangle "_doc sphinx_module\n(HTML documentation)" <> as docs + +A --> B +B --> conf +conf --> C +C --> report +report --> D +D --> html +report --> E +E --> rst +rst --> F +F --> index +report --> G +html --> G +rst --> G +index --> G +G --> gate +G --> docs + +@enduml diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index c388c85e..c3d4fd44 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -787,7 +787,11 @@ and scope checks at build/test time. * - ``maturity`` - string - no - - ``"release"`` (default) treats certified-scope violations and architecture consistency errors as hard build failures. ``"development"`` downgrades them to warnings so the build still succeeds — useful when iterating before all scope and architecture declarations are finalised. **Always revert to ``"release"`` before a certification delivery.** + - ``"release"`` (default) treats certified-scope violations, architecture consistency errors, and missing traceability links as hard build/test failures. ``"development"`` downgrades them to warnings so the build/test still succeeds — useful when iterating before all scope, architecture, and traceability declarations are finalised. **Always revert to ``"release"`` before a certification delivery.** + * - ``generate_html_report`` + - bool + - no + - If ``True``, additionally generate a standalone LOBSTER HTML traceability report. Off by default: nothing consumes it (Sphinx renders the multi-page RST report under ``traceability_report/`` instead), so it only adds a build action (default ``False``) * - ``testonly`` - bool - no diff --git a/bazel/rules/rules_score/docs/tooling_architecture.rst b/bazel/rules/rules_score/docs/tooling_architecture.rst index b764339b..c4f8daff 100644 --- a/bazel/rules/rules_score/docs/tooling_architecture.rst +++ b/bazel/rules/rules_score/docs/tooling_architecture.rst @@ -209,6 +209,35 @@ feed that pipeline: **FTA** (``fta.puml``) → ``puml_cli`` (FTA mode) → ``root_causes.lobster``. * **Unit tests** (gtest) → ``gtest_report`` → ``.lobster``. +Lobster report assembly (``dependable_element``) +------------------------------------------------- + +The ``config → report.json → gate`` box in the diagram above is itself a +multi-step pipeline, all inside the ``dependable_element`` ``_index`` rule. +Stages A–D turn the collected ``.lobster`` inputs into the aggregated report; +stages E–G render it as Sphinx documentation and expose it to the pass/fail +test and to other rules. + +.. uml:: _assets/lobster_report_assembly.puml + :align: center + :alt: How dependable_element assembles and publishes the lobster report + :width: 100% + +#. **Config** — ``format_lobster_block()`` fills a template with one block per + level, guarded by whether that level has sources (release mode force-emits + empty verification levels so a missing one still fails the gate). +#. **Report** — ``lobster-report`` aggregates all levels/inputs against that + config into ``report.json``, the single artefact everything below reads. +#. **HTML** — optional standalone convenience artefact. +#. **Multi-page RST** — ``lobster-rst-report --out-dir`` renders one page per + level plus an ``index.rst`` into a directory artefact. +#. **Splice** — the parent ``index.rst`` gets a ``Traceability`` toctree entry + pointing at that directory, but only if it was actually produced. +#. **Publish** — ``report.json`` (plus HTML/RST) are exposed via + ``DependableElementLobsterInfo`` and an output group, so the sibling + ```` test can gate on it (``lobster-ci-report``) and the sibling + ``_doc`` ``sphinx_module`` can render it into the final HTML. + .. _two-phase-sphinx-build: Two-phase Sphinx build diff --git a/bazel/rules/rules_score/examples/integrator/BUILD b/bazel/rules/rules_score/examples/integrator/BUILD index f060dd16..c963f8ad 100644 --- a/bazel/rules/rules_score/examples/integrator/BUILD +++ b/bazel/rules/rules_score/examples/integrator/BUILD @@ -71,6 +71,7 @@ component( # receives all forwarded AoUs: # - SampleType.SampleAoU (auto-forwarded, own AoU of seooc) # - OtherLibrary.TimingConstraint (chain-forwarded from other_seooc through seooc) + dependable_element( name = "integrator_seooc", architectural_design = [":integrator_design"], @@ -78,6 +79,7 @@ dependable_element( components = [":integrator_component"], dependability_analysis = [], integrity_level = "B", + maturity = "development", requirements = [ "//docs/requirements:feature_requirements", ], diff --git a/bazel/rules/rules_score/examples/integrator/MODULE.bazel b/bazel/rules/rules_score/examples/integrator/MODULE.bazel index f5e976c4..5b3c1b28 100644 --- a/bazel/rules/rules_score/examples/integrator/MODULE.bazel +++ b/bazel/rules/rules_score/examples/integrator/MODULE.bazel @@ -42,12 +42,7 @@ local_path_override( bazel_dep(name = "trlc", version = "3.0.0") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") -bazel_dep(name = "lobster", version = "1.0.3") -git_override( - module_name = "lobster", - commit = "07382c33f95e22992e0f8788c0537ec06877e51f", - remote = "https://github.com/bmw-software-engineering/lobster.git", -) +bazel_dep(name = "lobster", version = "1.0.4") # seooc's own llvm toolchain extension is a dev_dependency, so it is only # honored while seooc itself is the root module — it is stripped when seooc is diff --git a/bazel/rules/rules_score/examples/minimal/MODULE.bazel b/bazel/rules/rules_score/examples/minimal/MODULE.bazel index 3e143449..15f7f113 100644 --- a/bazel/rules/rules_score/examples/minimal/MODULE.bazel +++ b/bazel/rules/rules_score/examples/minimal/MODULE.bazel @@ -34,9 +34,4 @@ use_repo(llvm, "llvm_toolchain", "llvm_toolchain_llvm") bazel_dep(name = "trlc", version = "3.0.0") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") -bazel_dep(name = "lobster", version = "1.0.3") -git_override( - module_name = "lobster", - commit = "07382c33f95e22992e0f8788c0537ec06877e51f", - remote = "https://github.com/bmw-software-engineering/lobster.git", -) +bazel_dep(name = "lobster", version = "1.0.4") diff --git a/bazel/rules/rules_score/examples/seooc/MODULE.bazel b/bazel/rules/rules_score/examples/seooc/MODULE.bazel index 9dbb95dd..8fa6c228 100644 --- a/bazel/rules/rules_score/examples/seooc/MODULE.bazel +++ b/bazel/rules/rules_score/examples/seooc/MODULE.bazel @@ -42,9 +42,4 @@ use_repo(llvm, "llvm_toolchain", "llvm_toolchain_llvm") bazel_dep(name = "trlc", version = "3.0.0") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") -bazel_dep(name = "lobster", version = "1.0.3") -git_override( - module_name = "lobster", - commit = "07382c33f95e22992e0f8788c0537ec06877e51f", - remote = "https://github.com/bmw-software-engineering/lobster.git", -) +bazel_dep(name = "lobster", version = "1.0.4") 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 fae6335e..df39df77 100644 --- a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl +++ b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl @@ -1,39 +1,19 @@ -requirements "Feature Requirements" { -{FEAT_REQ_SOURCES} -} +{FEAT_REQ_BLOCK} -requirements "Forwarded AoUs" { -{FORWARDED_AOU_SOURCES} -} +{FORWARDED_AOU_BLOCK} -requirements "Component Requirements" { -{COMP_REQ_SOURCES}{COMP_REQ_TRACE}} +{COMP_REQ_BLOCK} -activity "Unit Test" { -{UNIT_TEST_SOURCES} -} +{UNIT_TEST_BLOCK} {COVERAGE_BLOCK} -implementation "Architecture" { -{ARCH_SOURCES} - trace to: "Component Requirements"; -} - -implementation "Public API" { -{PUBLIC_API_SOURCES} -} - -requirements "Failure Modes" { -{FM_SOURCES} - trace to: "Public API"; -} - -requirements "Control Measures" { -{CM_SOURCES} -} - -activity "Root Causes" { -{RC_SOURCES} - trace to: "Failure Modes"; - trace to: "Control Measures"; -} + +{ARCH_BLOCK} + +{PUBLIC_API_BLOCK} + +{FM_BLOCK} + +{CM_BLOCK} + +{RC_BLOCK} diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 152ac82d..d806153d 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -52,7 +52,7 @@ load( ) load( "//bazel/rules/rules_score/private:lobster_config.bzl", - "format_lobster_sources", + "format_lobster_block", ) load("//bazel/rules/rules_score/private:sphinx_module.bzl", "sphinx_module") load("//bazel/rules/rules_score/private:validation.bzl", "PROFILES", "VALIDATION_ATTRS", "run_validation") @@ -826,24 +826,12 @@ def _dependable_element_index_impl(ctx): # Generate submodule links for the index page deps_links = _process_deps(ctx) - # The traceability report is generated into {label.name}/traceability_report/, - # which is a sibling of index.rst inside the same {label.name}/ srcdir that - # Sphinx uses (srcdir = parent of --index_file). The toctree therefore - # references it as "traceability_report/index" with no "../" traversal. - # A lightweight provider-presence check mirrors the condition in the lobster - # block below; when no lobster inputs exist the substitution is empty so - # Sphinx does not complain about a missing document. - _has_feature_reqs = any([ - FeatureRequirementsInfo in r or AssumedSystemRequirementsInfo in r - for r in ctx.attr.requirements - ]) - _has_comp_reqs = any([ComponentInfo in c for c in ctx.attr.components]) - traceability_entries = ( - ["traceability_report/index"] if _has_feature_reqs and _has_comp_reqs else [] - ) - # Generate one section page per top-level group so each becomes a proper # clickable sidebar entry. Each block is explicit and independently controlled. + # Note: the "Traceability" section page is built later, after the lobster + # traceability block below determines whether traceability_report/index.rst + # actually gets generated (see the `_section_page("traceability", ...)` call + # near the end of this function). def _section_page(name, title, entries, maxdepth = 1): """Declare + expand one section page; return filename or None when empty.""" @@ -878,11 +866,6 @@ def _dependable_element_index_impl(ctx): "Components", component_entries, ) - traceability_ref = _section_page( - "traceability", - "Traceability", - traceability_entries, - ) checklists_ref = _section_page( "checklists", "Checklists", @@ -896,34 +879,6 @@ def _dependable_element_index_impl(ctx): maxdepth = 2, ) - section_refs = [ - ref - for ref in [ - assumed_system_ref, - software_arch_ref, - components_ref, - traceability_ref, - checklists_ref, - glossary_ref, - ] - if ref != None - ] - - # Render the index.rst using the template - title = ctx.attr.module_name - underline = "=" * len(title) - - ctx.actions.expand_template( - template = ctx.file.template, - output = index_rst, - substitutions = { - "{title}": title, - "{underline}": underline, - "{sections}": "\n ".join(section_refs), - "{submodules}": deps_links, - }, - ) - # ========================================================================= # Architecture Verification: build current-architecture JSON and run validation # ========================================================================= @@ -1041,9 +996,16 @@ def _dependable_element_index_impl(ctx): # ========================================================================= # Lobster Traceability: Dependable Element Level - # Builds a three-tier report: Feature Requirements <- Component Requirements - # <- Unit Tests (gtest). The report is only produced when all three tiers - # are present; gtest is optional (component-only traceability is possible). + # Builds the multi-level traceability report (Feature/Assumed-System + # Requirements, Component Requirements, Unit Test, Test Case Coverage, + # Architecture, Public API, Failure Modes, Control Measures, Root Causes). + # The report is produced whenever any level has data; each level and its + # `trace to:` coverage edges are emitted only when relevant. In release + # mode every checking level (Unit Test, Test Case Coverage, Architecture, + # Failure Modes, Root Causes) is emitted even when empty, so a missing + # unit test / coverage / architecture allocation / public API + # characterization / root cause fails the traceability check (see the + # strict handling below). # ========================================================================= # Collect feature requirement .lobster files from requirements targets @@ -1219,47 +1181,134 @@ def _dependable_element_index_impl(ctx): lobster_html_report = None lobster_rst_dir = None lobster_files = [] - if feat_req_list: - # Build comp_req trace-to lines (Feature Requirements + optionally Forwarded AoUs) - comp_req_trace_lines = "" - if comp_req_list: - comp_req_trace_lines = " trace to: \"Feature Requirements\";\n" - lobster_config = ctx.actions.declare_file(ctx.label.name + "/de_traceability_config") + # Generate whenever there is at least one lobster input; individual + # levels (e.g. Feature Requirements, Failure Modes) are all optional and + # each one, along with any `trace to:` line pointing at it, is only + # emitted into the config when it actually has sources (see + # format_lobster_block). This keeps the report free of false "missing + # reference" issues for levels a given dependable element does not use. + has_any_lobster_input = any([ + feat_req_list, + received_aou_list, + comp_req_list, + comp_test_list, + coverage_lobster_files, + comp_arch_list, + interface_req_list, + fm_list, + cm_list, + rc_list, + ]) - # Only emit the "Test Case Coverage" LOBSTER layer when there is - # actual coverage data; an empty layer with `trace to` would cause - # LOBSTER to report every component requirement as missing a reference. - if coverage_lobster_files: - coverage_block = ( - "activity \"Test Case Coverage\" {\n" + - format_lobster_sources(coverage_lobster_files) + "\n" + - " trace to: \"Component Requirements\";\n}" - ) - else: - coverage_block = "" + if has_any_lobster_input: + # Release mode enforces a strict, uniform coverage policy: every + # LOBSTER level that acts as a *checking* source for another level's + # coverage (i.e. declares `trace to: ""`) is declared even + # when it has no sources of its own. LOBSTER's coverage check runs + # per checking-level: with zero items in the checking level, every + # item in the target level is reported as a "missing down reference". + # This is what makes a genuinely missing downstream artifact fail the + # build instead of the trace_to edge silently disappearing when the + # checking level's inputs happen to be empty (the exact bug this + # rework fixes). Concretely, in release mode this applies to: + # * Unit Test, Test Case Coverage, Architecture + # -> check Component Requirements coverage + # * Failure Modes + # -> check Public API coverage + # * Root Causes + # -> check Failure Modes and Control Measures coverage + # In development mode these checking levels are omitted when empty + # instead, keeping the in-progress report free of noise. + # + # Each `trace to:` target level must itself be present in the config + # for LOBSTER to accept the edge (an edge to an absent level is an + # "unknown tracing target" error, not a coverage failure). So a + # target level that could otherwise be omitted (Public API) is also + # force-emitted empty in release mode purely to remain a valid + # target -- it does not itself become a checking level. + strict = ctx.attr.maturity == "release" + + has_feat_req = bool(feat_req_list) + has_comp_req = bool(comp_req_list) + has_public_api = bool(interface_req_list) or strict + has_fm = bool(fm_list) or strict + has_cm = bool(cm_list) or strict + + lobster_config = ctx.actions.declare_file(ctx.label.name + "/de_traceability_config") ctx.actions.expand_template( template = ctx.file._lobster_de_template, output = lobster_config, substitutions = { - "{FEAT_REQ_SOURCES}": format_lobster_sources(feat_req_list), - "{FORWARDED_AOU_SOURCES}": format_lobster_sources(received_aou_list), - "{COMP_REQ_SOURCES}": format_lobster_sources(comp_req_list), - "{COMP_REQ_TRACE}": comp_req_trace_lines, - "{ARCH_SOURCES}": format_lobster_sources(comp_arch_list), - "{UNIT_TEST_SOURCES}": format_lobster_sources(comp_test_list), - "{COVERAGE_BLOCK}": coverage_block, - "{PUBLIC_API_SOURCES}": format_lobster_sources(interface_req_list), - "{FM_SOURCES}": format_lobster_sources(fm_list), - "{CM_SOURCES}": format_lobster_sources(cm_list), - "{RC_SOURCES}": format_lobster_sources(rc_list), + "{FEAT_REQ_BLOCK}": format_lobster_block("requirements", "Feature Requirements", feat_req_list), + "{FORWARDED_AOU_BLOCK}": format_lobster_block("requirements", "Forwarded AoUs", received_aou_list), + "{COMP_REQ_BLOCK}": format_lobster_block( + "requirements", + "Component Requirements", + comp_req_list, + trace_to = ["Feature Requirements"] if has_feat_req else [], + ), + "{UNIT_TEST_BLOCK}": format_lobster_block( + "activity", + "Unit Test", + comp_test_list, + trace_to = ["Component Requirements"] if has_comp_req else [], + emit_empty = strict, + ), + "{COVERAGE_BLOCK}": format_lobster_block( + "activity", + "Test Case Coverage", + coverage_lobster_files, + trace_to = ["Component Requirements"] if has_comp_req else [], + emit_empty = strict, + ), + "{ARCH_BLOCK}": format_lobster_block( + "implementation", + "Architecture", + comp_arch_list, + trace_to = ["Component Requirements"] if has_comp_req else [], + emit_empty = strict, + ), + "{PUBLIC_API_BLOCK}": format_lobster_block( + "implementation", + "Public API", + interface_req_list, + emit_empty = strict, + ), + "{FM_BLOCK}": format_lobster_block( + "requirements", + "Failure Modes", + fm_list, + trace_to = ["Public API"] if has_public_api else [], + emit_empty = strict, + ), + "{CM_BLOCK}": format_lobster_block( + "requirements", + "Control Measures", + cm_list, + emit_empty = strict, + ), + "{RC_BLOCK}": format_lobster_block( + "activity", + "Root Causes", + rc_list, + trace_to = (["Failure Modes"] if has_fm else []) + (["Control Measures"] if has_cm else []), + emit_empty = strict, + ), }, ) 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 lobster_report_file = subrule_lobster_report(all_lobster_inputs, lobster_config) - lobster_html_report = subrule_lobster_html_report(lobster_report_file) + lobster_files = [lobster_config, lobster_report_file] + + # The HTML report is a standalone convenience artifact: nothing else + # in the build consumes it (Sphinx renders the RST report below), so + # it is only produced when explicitly requested. + if ctx.attr.generate_html_report: + lobster_html_report = subrule_lobster_html_report(lobster_report_file) + lobster_files.append(lobster_html_report) # Generate multi-page RST report inside the index subdirectory so that # Sphinx (which uses {label.name}/index.rst's parent as srcdir) can @@ -1285,9 +1334,50 @@ def _dependable_element_index_impl(ctx): progress_message = "lobster-rst-report (pages) {}".format(ctx.label.name), ) - lobster_files = [lobster_config, lobster_report_file, lobster_html_report, lobster_rst_dir] + lobster_files.append(lobster_rst_dir) output_files.extend(lobster_files) + # Now that lobster_rst_dir's final value is known, finish the Traceability + # section page and render index.rst. This must come after the lobster + # block above so the toctree entry is added if and only if + # traceability_report/index.rst actually gets generated. + traceability_entries = ( + ["traceability_report/index"] if lobster_rst_dir != None else [] + ) + traceability_ref = _section_page( + "traceability", + "Traceability", + traceability_entries, + ) + + section_refs = [ + ref + for ref in [ + assumed_system_ref, + software_arch_ref, + components_ref, + traceability_ref, + checklists_ref, + glossary_ref, + ] + if ref != None + ] + + # Render the index.rst using the template + title = ctx.attr.module_name + underline = "=" * len(title) + + ctx.actions.expand_template( + template = ctx.file.template, + output = index_rst, + substitutions = { + "{title}": title, + "{underline}": underline, + "{sections}": "\n ".join(section_refs), + "{submodules}": deps_links, + }, + ) + return [ DefaultInfo(files = depset(output_files)), SphinxIndexFileInfo(index_file = index_rst), @@ -1382,6 +1472,10 @@ def _dependable_element_index_attrs(): values = ["release", "development"], doc = "Maturity level of the dependable element. 'release' (default) treats certified scope violations as errors; 'development' emits warnings and continues.", ), + "generate_html_report": attr.bool( + default = False, + doc = "Whether to additionally generate a standalone LOBSTER HTML traceability report. Off by default: it is not consumed anywhere (Sphinx renders the multi-page RST report instead) and only adds a build action.", + ), "_lobster_de_template": attr.label( default = Label("//bazel/rules/rules_score/lobster/config:lobster_de_template"), allow_single_file = True, @@ -1439,10 +1533,19 @@ def _dependable_element_impl(ctx): test_executable = ctx.actions.declare_file(ctx.label.name + "_lobster_test") if lobster_info.lobster_report != None: - command = "set -o pipefail; {ci} {report}".format( - ci = ctx.executable._lobster_ci_report.short_path, - report = lobster_info.lobster_report.short_path, - ) + if ctx.attr.maturity == "development": + # Development mode: report traceability issues without failing the + # build, matching the certified-scope / validation warning pattern + # used elsewhere in this rule. + command = "{ci} {report} || echo 'WARNING: lobster traceability check failed (maturity=development)'".format( + ci = ctx.executable._lobster_ci_report.short_path, + report = lobster_info.lobster_report.short_path, + ) + else: + command = "set -o pipefail; {ci} {report}".format( + ci = ctx.executable._lobster_ci_report.short_path, + report = lobster_info.lobster_report.short_path, + ) runfiles = ctx.runfiles( files = [ctx.executable._lobster_ci_report, lobster_info.lobster_report], ).merge(ctx.attr._lobster_ci_report[DefaultInfo].default_runfiles) @@ -1456,9 +1559,10 @@ def _dependable_element_impl(ctx): is_executable = True, ) - # Compose default outputs: the two lobster report files (JSON + HTML) and - # the Sphinx HTML documentation. Intermediate files from the index (RST - # sources, lobster config, etc.) are intentionally excluded. + # Compose default outputs: the lobster JSON report (plus the HTML report + # when generate_html_report is enabled) and the Sphinx HTML documentation. + # Intermediate files from the index (RST sources, lobster config, etc.) + # are intentionally excluded. lobster_default_files = [] if lobster_info.lobster_report: lobster_default_files.append(lobster_info.lobster_report) @@ -1466,7 +1570,7 @@ def _dependable_element_impl(ctx): lobster_default_files.append(lobster_info.lobster_html_report) return [ - # DefaultInfo: two lobster report files + Sphinx HTML docs so that + # DefaultInfo: lobster report file(s) + Sphinx HTML docs so that # ``bazel build `` produces exactly the final user-facing outputs. DefaultInfo( executable = test_executable, @@ -1499,6 +1603,11 @@ _dependable_element_test = rule( mandatory = True, doc = "The _doc sphinx_module target providing SphinxModuleInfo.", ), + "maturity": attr.string( + default = "release", + values = ["release", "development"], + doc = "Maturity level of the dependable element. 'release' (default) fails ``bazel test`` when traceability links are missing; 'development' emits a warning and continues.", + ), "_lobster_ci_report": attr.label( default = Label("@lobster//:lobster-ci-report"), executable = True, @@ -1527,6 +1636,7 @@ def dependable_element( deps = [], aou_forwarding = None, maturity = "release", + generate_html_report = False, testonly = True, docs_prefix = "docs/sphinx/", **kwargs): @@ -1567,6 +1677,13 @@ def dependable_element( aou_forwarding: Optional label to a YAML file listing received AoU IDs to further-forward to this element's own dependees. Only needed for chain-forwarding received AoUs that this element cannot handle. + maturity: 'release' (default) fails the build/test on certified-scope, + architecture-consistency, or traceability violations. 'development' + downgrades all of these to warnings so the build/test still succeeds. + generate_html_report: If True, additionally generate a standalone + LOBSTER HTML traceability report. Off by default: it is not + consumed anywhere (Sphinx renders the multi-page RST report + instead) and only adds a build action. testonly: If True, only testonly targets can depend on this target. docs_prefix: Prefix under which the generated `_rst` sphinx_docs_library exposes its RST sources, so that an external Sphinx build can embed it via @@ -1605,6 +1722,7 @@ def dependable_element( aou_forwarding = aou_forwarding, integrity_level = integrity_level, maturity = maturity, + generate_html_report = generate_html_report, testonly = testonly, **kwargs ) @@ -1629,6 +1747,7 @@ def dependable_element( name = name, index_dep = ":" + name + "_index", sphinx_module_dep = ":" + name + "_doc", + maturity = maturity, **kwargs ) diff --git a/bazel/rules/rules_score/private/lobster_config.bzl b/bazel/rules/rules_score/private/lobster_config.bzl index 6a1924fb..d201e084 100644 --- a/bazel/rules/rules_score/private/lobster_config.bzl +++ b/bazel/rules/rules_score/private/lobster_config.bzl @@ -31,3 +31,46 @@ def format_lobster_sources(files): for substituting into a lobster config template placeholder. """ return "\n".join([' source: "{}";'.format(f.path) for f in files]) + +def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): + """Build a complete, optional LOBSTER config block. + + By default returns "" (omitting the level entirely) when `files` is empty, + instead of emitting an empty block. An empty level that other levels + `trace to:` would make LOBSTER report every item at those other levels as + missing a reference, so callers must only pass names in `trace_to` for + levels that are themselves non-empty (i.e. also present in the same config). + + Set `emit_empty = True` to declare the level even when it has no sources. + This keeps the level's `trace to:` policy edge active so that LOBSTER + reports a "missing down reference" for every item at the target level that + this (now source-less) level fails to cover — used in release mode to make + a missing verification level (e.g. no unit tests, no root causes) fail the + traceability check instead of silently disappearing from the policy. + + Args: + kind: LOBSTER block kind, e.g. "requirements", "activity", "implementation". + name: Level name, used as the quoted block header. + files: List of File objects to include as sources; the block is + omitted entirely (returns "") when this list is empty, unless + `emit_empty` is True. + trace_to: List of level names this level traces to. Only include + names of levels that are themselves present (non-empty) in the + same config. + 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. + + Returns: + The full block text (kind, header, sources, trace-to lines), or "" + when `files` is empty and `emit_empty` is 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( + kind = kind, + name = name, + sources = format_lobster_sources(files), + trace = trace_lines, + ) diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 26f849f0..80cff397 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -43,6 +43,10 @@ load( "providers_test", "sphinx_module_test_suite", ) +load( + ":lobster_config_test.bzl", + "lobster_config_test_suite", +) load( ":requirements_image_test.bzl", "image_srcs_sphinx_sources_test", @@ -691,7 +695,11 @@ component( tests = [], # Empty for testing ) -# Test Dependable Element +# Test Dependable Element. +# maturity = "development": this fixture intentionally omits a +# test_case_coverage.lock.yaml, so under the release traceability gate its +# empty "Test Case Coverage" level would be reported as a missing element. +# Development maturity relaxes that gate. dependable_element( name = "test_dependable_element", testonly = True, @@ -700,6 +708,7 @@ dependable_element( components = [":test_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", + maturity = "development", requirements = [":feat_req"], tests = [], # Empty for testing ) @@ -793,7 +802,10 @@ component( tests = [], ) -# Dependable element with nested components to test recursive collection +# Dependable element with nested components to test recursive collection. +# maturity = "development": like ``test_dependable_element`` it has no +# test_case_coverage.lock.yaml, so the release gate would flag a missing +# Test Case Coverage element. dependable_element( name = "test_dependable_element_nested", testonly = True, @@ -802,6 +814,7 @@ dependable_element( components = [":test_parent_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", + maturity = "development", requirements = [":feat_req"], tests = [], ) @@ -1278,6 +1291,11 @@ aous_rst_sphinx_test( requirements_rst_test_suite(name = "requirements_rst_tests") +# ============================================================================ +# lobster_config.bzl unit tests (format_lobster_sources / format_lobster_block) +# ============================================================================ +lobster_config_test_suite(name = "lobster_config") + # ============================================================================ # Image srcs Tests (defined in sub-package to avoid workspace-root edge case) # ============================================================================ @@ -1353,6 +1371,7 @@ py_test( test_suite( name = "all_tests", tests = [ + ":lobster_config_tests", ":requirements_rst_tests", ":seooc_tests", ":sphinx_module_tests", diff --git a/bazel/rules/rules_score/test/MODULE.bazel b/bazel/rules/rules_score/test/MODULE.bazel index 64e6652b..7ff05d0e 100644 --- a/bazel/rules/rules_score/test/MODULE.bazel +++ b/bazel/rules/rules_score/test/MODULE.bazel @@ -103,9 +103,4 @@ register_toolchains( ) bazel_dep(name = "trlc", version = "3.0.0") -bazel_dep(name = "lobster", version = "1.0.3") -git_override( - module_name = "lobster", - commit = "07382c33f95e22992e0f8788c0537ec06877e51f", - remote = "https://github.com/bmw-software-engineering/lobster.git", -) +bazel_dep(name = "lobster", version = "1.0.4") diff --git a/bazel/rules/rules_score/test/lobster_config_test.bzl b/bazel/rules/rules_score/test/lobster_config_test.bzl new file mode 100644 index 00000000..c9cae8be --- /dev/null +++ b/bazel/rules/rules_score/test/lobster_config_test.bzl @@ -0,0 +1,152 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +""" +Loading-phase unit tests for format_lobster_sources()/format_lobster_block() +(bazel/rules/rules_score/private/lobster_config.bzl). + +Both functions are pure Starlark -- they only read `.path` off whatever is +passed as a "file" -- so plain `struct(path = ...)` fakes stand in for real +File objects, and skylib's `loadingtest` (not `analysistest`) is used: no +target_under_test / analysis phase is needed to exercise them. +""" + +load("@bazel_skylib//lib:unittest.bzl", "loadingtest") +load( + "@score_tooling//bazel/rules/rules_score/private:lobster_config.bzl", + "format_lobster_block", + "format_lobster_sources", +) + +def _fake_file(path): + return struct(path = path) + +def lobster_config_test_suite(name): + """Defines the loading-phase test suite for lobster_config.bzl helpers. + + Args: + name: Suite name; individual test targets and the aggregating + `_tests` test_suite are derived from it. + """ + env = loadingtest.make(name) + + # --- format_lobster_sources ---------------------------------------------- + + loadingtest.equals( + env, + "sources_empty", + "", + format_lobster_sources([]), + ) + + loadingtest.equals( + env, + "sources_multiple", + ' source: "a.lobster";\n source: "b.lobster";', + format_lobster_sources([_fake_file("a.lobster"), _fake_file("b.lobster")]), + ) + + # --- format_lobster_block: empty level is omitted by default ------------- + # This is the core bug fix: an empty level must not appear at all (and + # thus carry no `trace to:` edge), instead of rendering an empty block + # that would make LOBSTER flag every item at the target level as missing + # a reference. + + loadingtest.equals( + env, + "block_empty_omitted", + "", + format_lobster_block("requirements", "Feature Requirements", []), + ) + + loadingtest.equals( + env, + "block_empty_omitted_even_with_trace_to", + "", + format_lobster_block( + "requirements", + "Component Requirements", + [], + trace_to = ["Feature Requirements"], + ), + ) + + # --- format_lobster_block: emit_empty keeps the trace-to edge active ----- + # Used by callers in strict/release mode to force a checking level (e.g. + # Unit Test) to exist even without sources, so LOBSTER still reports + # missing coverage of the target level instead of silently skipping it. + + loadingtest.equals( + env, + "block_empty_emit_empty_keeps_trace_to", + 'activity "Unit Test" {\n\n trace to: "Component Requirements";\n}', + format_lobster_block( + "activity", + "Unit Test", + [], + trace_to = ["Component Requirements"], + emit_empty = True, + ), + ) + + loadingtest.equals( + env, + "block_empty_emit_empty_no_trace_to", + 'implementation "Public API" {\n\n}', + format_lobster_block( + "implementation", + "Public API", + [], + emit_empty = True, + ), + ) + + # --- format_lobster_block: non-empty level --------------------------------- + + loadingtest.equals( + env, + "block_with_sources_no_trace", + 'requirements "Feature Requirements" {\n source: "feat.lobster";\n}', + format_lobster_block("requirements", "Feature Requirements", [_fake_file("feat.lobster")]), + ) + + # --- format_lobster_block: non-empty level, guarded trace_to present ------- + # This is the "guarded" half of the fix: callers only pass a `trace_to` + # name when that target level is itself non-empty in the same config; + # this case verifies the trace line renders correctly once a caller does + # pass one. + + loadingtest.equals( + env, + "block_with_sources_and_guarded_trace_to", + 'requirements "Component Requirements" {\n source: "comp.lobster";\n trace to: "Feature Requirements";\n}', + format_lobster_block( + "requirements", + "Component Requirements", + [_fake_file("comp.lobster")], + trace_to = ["Feature Requirements"], + ), + ) + + # --- format_lobster_block: multiple trace_to targets ----------------------- + + loadingtest.equals( + env, + "block_with_multiple_trace_to", + 'activity "Root Causes" {\n source: "rc.lobster";\n trace to: "Failure Modes";\n trace to: "Control Measures";\n}', + format_lobster_block( + "activity", + "Root Causes", + [_fake_file("rc.lobster")], + trace_to = ["Failure Modes", "Control Measures"], + ), + )