diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8f4e440..5c8eb354 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,6 +20,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6.0.2 + - name: Install Missing Dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev - name: Run python_basics integration tests run: | cd python_basics/integration_tests @@ -38,3 +42,9 @@ jobs: - name: Run rules_score tests run: | bazel test //bazel/rules/rules_score/... + - name: Run Plantuml Tooling tests + run: | + bazel test //plantuml/... + - name: Run Validation Tooling tests + run: | + bazel test //validation/... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c17df6b6..5f99366c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: check-shebang-scripts-are-executable - id: check-executables-have-shebangs - id: check-added-large-files - args: [--maxkb=50, --enforce-all] # increase or add git lfs if too strict + args: [--maxkb=100, --enforce-all] # increase or add git lfs if too strict exclude: org.eclipse.dash.licenses-1.1.0.jar|blanket_index.html - repo: https://github.com/google/yamlfmt rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 diff --git a/MODULE.bazel b/MODULE.bazel index 873df5df..689a648e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -255,7 +255,7 @@ git_override( bazel_dep(name = "lobster", version = "0.0.0") git_override( module_name = "lobster", - commit = "94ed5961ca28ee1b840cd8a938138c17ae4da671", + commit = "d528fbdec2cd72ff7967b51546fb0bd935810258", remote = "https://github.com/bmw-software-engineering/lobster.git", ) diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index ac6ca464..a853003c 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -88,7 +88,7 @@ py_binary( visibility = ["//visibility:public"], deps = [ ":sphinx_module_ext", - "//tools/sphinx/extensions/lobster", + "@lobster//sphinx_lobster:sphinx_lobster_builder", "@score_docs_as_code//src/extensions/score_metamodel", "@score_tooling//plantuml/sphinx/clickable_plantuml", "@trlc//tools/sphinx/extensions/trlc", @@ -132,7 +132,7 @@ py_binary( main = "src/sphinx_wrapper.py", visibility = ["//visibility:public"], deps = [ - "//tools/sphinx/extensions/lobster", + "@lobster//sphinx_lobster:sphinx_lobster_builder", "@score_docs_as_code//src/extensions/score_metamodel", "@score_tooling//plantuml/sphinx/clickable_plantuml", "@trlc//tools/sphinx/extensions/trlc", diff --git a/bazel/rules/rules_score/config/BUILD b/bazel/rules/rules_score/lobster/config/BUILD similarity index 100% rename from bazel/rules/rules_score/config/BUILD rename to bazel/rules/rules_score/lobster/config/BUILD diff --git a/bazel/rules/rules_score/config/lobster_comp_req.yaml b/bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml similarity index 100% rename from bazel/rules/rules_score/config/lobster_comp_req.yaml rename to bazel/rules/rules_score/lobster/config/lobster_comp_req.yaml diff --git a/bazel/rules/rules_score/config/lobster_component.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl similarity index 100% rename from bazel/rules/rules_score/config/lobster_component.conf.tpl rename to bazel/rules/rules_score/lobster/config/lobster_component.conf.tpl diff --git a/bazel/rules/rules_score/config/lobster_controlmeasures.yaml b/bazel/rules/rules_score/lobster/config/lobster_controlmeasures.yaml similarity index 100% rename from bazel/rules/rules_score/config/lobster_controlmeasures.yaml rename to bazel/rules/rules_score/lobster/config/lobster_controlmeasures.yaml diff --git a/bazel/rules/rules_score/config/lobster_de.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl similarity index 100% rename from bazel/rules/rules_score/config/lobster_de.conf.tpl rename to bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl diff --git a/bazel/rules/rules_score/config/lobster_failuremodes.yaml b/bazel/rules/rules_score/lobster/config/lobster_failuremodes.yaml similarity index 100% rename from bazel/rules/rules_score/config/lobster_failuremodes.yaml rename to bazel/rules/rules_score/lobster/config/lobster_failuremodes.yaml diff --git a/bazel/rules/rules_score/config/lobster_feat_req.yaml b/bazel/rules/rules_score/lobster/config/lobster_feat_req.yaml similarity index 100% rename from bazel/rules/rules_score/config/lobster_feat_req.yaml rename to bazel/rules/rules_score/lobster/config/lobster_feat_req.yaml diff --git a/bazel/rules/rules_score/config/lobster_sa.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_sa.conf.tpl similarity index 100% rename from bazel/rules/rules_score/config/lobster_sa.conf.tpl rename to bazel/rules/rules_score/lobster/config/lobster_sa.conf.tpl diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index 12de20bb..a04eb5ac 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -234,7 +234,7 @@ _component_test = rule( 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/config:lobster_component_template"), + default = Label("//bazel/rules/rules_score/lobster/config:lobster_component_template"), allow_single_file = True, doc = "Lobster config template for component traceability.", ), diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 4ed89d95..233caa0b 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -221,7 +221,7 @@ _dependability_analysis_test = rule( doc = "lobster-ci-report executable for test execution.", ), "_lobster_sa_template": attr.label( - default = Label("//bazel/rules/rules_score/config:lobster_sa_template"), + default = Label("//bazel/rules/rules_score/lobster/config:lobster_sa_template"), allow_single_file = True, doc = "Lobster config template for safety analysis traceability.", ), diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index cf561577..3dbc8344 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1031,7 +1031,7 @@ _dependable_element_index = rule( doc = "Architecture verifier tool", ), "_lobster_de_template": attr.label( - default = Label("//bazel/rules/rules_score/config:lobster_de_template"), + default = Label("//bazel/rules/rules_score/lobster/config:lobster_de_template"), allow_single_file = True, doc = "Lobster config template for dependable element traceability.", ), diff --git a/bazel/rules/rules_score/private/fmea.bzl b/bazel/rules/rules_score/private/fmea.bzl index c1403cfd..64b953fd 100644 --- a/bazel/rules/rules_score/private/fmea.bzl +++ b/bazel/rules/rules_score/private/fmea.bzl @@ -347,12 +347,12 @@ _fmea = rule( doc = "lobster-trlc executable used to generate FM and CM lobster files.", ), "_fm_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/config:failuremodes_config"), + default = Label("//bazel/rules/rules_score/lobster/config:failuremodes_config"), allow_single_file = True, doc = "lobster-trlc YAML config for FailureMode records.", ), "_cm_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/config:controlmeasures_config"), + default = Label("//bazel/rules/rules_score/lobster/config:controlmeasures_config"), allow_single_file = True, doc = "lobster-trlc YAML config for ControlMeasure records.", ), diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index b4579d91..420ba8cd 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -134,7 +134,7 @@ def feature_requirements( _requirements( name = name, srcs = srcs, - lobster_config = "//bazel/rules/rules_score/config:feature_requirement", + lobster_config = Label("//bazel/rules/rules_score/lobster/config:feature_requirement"), req_kind = "feature", visibility = visibility, ) @@ -159,7 +159,7 @@ def component_requirements( _requirements( name = name, srcs = srcs, - lobster_config = "//bazel/rules/rules_score/config:component_requirement", + lobster_config = Label("//bazel/rules/rules_score/lobster/config:component_requirement"), req_kind = "component", visibility = visibility, ) diff --git a/tools/sphinx/extensions/BUILD b/tools/sphinx/extensions/BUILD deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/sphinx/extensions/lobster/BUILD b/tools/sphinx/extensions/lobster/BUILD deleted file mode 100644 index 2acc5c02..00000000 --- a/tools/sphinx/extensions/lobster/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -py_library( - name = "lobster", - srcs = ["lobster.py"], - imports = ["."], - visibility = [ - "//bazel/rules/rules_score:__pkg__", - "//tools/sphinx:__pkg__", - ], -) diff --git a/tools/sphinx/extensions/lobster/README.md b/tools/sphinx/extensions/lobster/README.md deleted file mode 100644 index 3dbf064d..00000000 --- a/tools/sphinx/extensions/lobster/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Sphinx Lobster Extension - -Integrates LOBSTER traceability into Sphinx documentation builds. - -## Usage - -The extension registers a custom Sphinx builder named `lobster`. It is used -automatically by the `rules_score` Sphinx build integration. - -The builder scans RST documents for `:requirement:upstream-ref:` roles and -produces a `_merged.lobster` file that feeds into the LOBSTER traceability chain. - -## Flow - -``` -Sphinx build (builder=lobster) - │ - ▼ -LobsterBuilder (subclass of TextBuilder) - - LobsterTranslator parses :requirement:upstream-ref: roles - - Each .rst file → /.json (lobster-imp-trace fragment) - │ - ▼ -LobsterBuilder.finish() - - Merges all per-doc JSON fragments into /_merged.lobster - │ - ▼ -OutputGroupInfo(lobster = []) - (returned by the sphinx Bazel rule) - │ - ▼ -sphinx_lobster_merge rule (tools/lobster/sphinx_lobster.bzl) - - pure provider adapter - │ - ▼ -LobsterProvider(lobster_input = {"sphinx_docs.lobster": "/_merged.lobster"}) -``` - -## Architecture - -The extension consists of two classes in `lobster.py`: - -- **`LobsterTranslator`** (`SphinxTranslator` subclass) — visits the RST document tree, - collects `upstream-ref` and `downstream-ref` role references, and builds a lobster - JSON structure per document. -- **`LobsterBuilder`** (`TextBuilder` subclass) — Sphinx builder that writes one `.json` - file per document, then merges all fragments into `_merged.lobster` in its `finish()` hook. diff --git a/tools/sphinx/extensions/lobster/lobster.py b/tools/sphinx/extensions/lobster/lobster.py deleted file mode 100644 index 6fbd25e4..00000000 --- a/tools/sphinx/extensions/lobster/lobster.py +++ /dev/null @@ -1,202 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -from __future__ import annotations - -from os import path -from pathlib import Path -from typing import TYPE_CHECKING - -from sphinx.builders.text import TextBuilder -from sphinx.locale import __ -from sphinx.util.docutils import SphinxTranslator -from docutils import nodes - -import json - - -class LobsterTranslator(SphinxTranslator): - builder: LobsterBuilder - - def __init__(self, document: nodes.document, builder: LobsterBuilder) -> None: - super().__init__(document, builder) - - self.source = "" - self.docname = "" - self.line = "" - self.section_path = [] - self.tags = {} - - self.lobster_json = {} - - self.lobster_json["data"] = [] - - self.lobster_json["generator"] = "lobster_sphinx" - self.lobster_json["schema"] = "lobster-imp-trace" - self.lobster_json["version"] = 3 - - def _current_tag_name(self) -> str: - section_str = ".".join(map(str, self.section_path)) - return self.docname + "." + section_str if self.docname else section_str - - def _ensure_tag(self, tag_name: str) -> None: - """Create a lobster item for tag_name if it does not exist yet.""" - if tag_name not in self.tags: - self.tags[tag_name] = {} - self.tags[tag_name]["tag"] = "sphinx " + tag_name - - loc = {} - loc["kind"] = "file" - loc["file"] = self.source - loc["line"] = ( - self.line if (isinstance(self.line, int) and self.line >= 1) else None - ) - loc["column"] = None - - self.tags[tag_name]["location"] = loc - - self.tags[tag_name]["name"] = tag_name - self.tags[tag_name]["messages"] = [] - self.tags[tag_name]["just_up"] = [] - self.tags[tag_name]["just_down"] = [] - self.tags[tag_name]["just_global"] = [] - self.tags[tag_name]["language"] = "rst" - self.tags[tag_name]["kind"] = "section" - self.tags[tag_name]["refs"] = [] - - self.lobster_json["data"].append(self.tags[tag_name]) - - def add_ref(self, name: str) -> None: - """Record an upstream tracing reference (upstream-ref role).""" - tag_name = self._current_tag_name() - self._ensure_tag(tag_name) - if name not in self.tags[tag_name]["refs"]: - self.tags[tag_name]["refs"].append(name) - - def add_downstream_ref(self, name: str) -> None: - """Record a downstream reference (downstream-ref role). - - Downstream refs are stored in just_down so lobster does not try to - resolve them as upstream tracing targets. - """ - tag_name = self._current_tag_name() - self._ensure_tag(tag_name) - if name not in self.tags[tag_name]["just_down"]: - self.tags[tag_name]["just_down"].append(name) - - def visit_document(self, node: Element) -> None: - self.source = node.attributes["source"] - # Derive a short doc identifier from the source path. - # Bazel sandboxes produce paths like .../.../_sources/sub/doc.rst - source = self.source - if "_sources/" in source: - doc_rel = source.split("_sources/", 1)[1] - else: - doc_rel = Path(source).name - if doc_rel.endswith(".rst"): - doc_rel = doc_rel[:-4] - # Convert slashes to dots and remove spaces for a clean identifier. - self.docname = doc_rel.replace("/", ".").replace(" ", "") - - def depart_document(self, node: Element) -> None: - self.body = json.dumps(self.lobster_json, indent=4) - - def visit_comment(self, node: Element) -> None: # type: ignore[override] - raise nodes.SkipNode - - def visit_toctree(self, node: Element) -> None: - raise nodes.SkipNode - - def visit_index(self, node: Element) -> None: - raise nodes.SkipNode - - def visit_tabular_col_spec(self, node: Element) -> None: - raise nodes.SkipNode - - def visit_pending_xref(self, node: Element) -> None: - # pending_xref nodes are normally resolved before write_doc runs, - # but handle them here as a fallback. - if node.get("refdomain") == "requirement": - if node.get("reftype") == "upstream-ref": - self.add_ref("req " + node.get("reftarget", "")) - elif node.get("reftype") == "downstream-ref": - self.add_downstream_ref("req " + node.get("reftarget", "")) - raise nodes.SkipNode - - def visit_literal(self, node: Element) -> None: - # Resolved xref roles become literal nodes whose classes carry the - # role name: 'requirement-upstream-ref' or 'requirement-downstream-ref'. - # Prefix with "req " to match the lobster-trlc requirement tag format. - classes = node.get("classes", []) - ref = "req " + node.astext() - if "requirement-upstream-ref" in classes: - self.add_ref(ref) - elif "requirement-downstream-ref" in classes: - self.add_downstream_ref(ref) - - def visit_paragraph(self, node: Element) -> None: - self.line = node.line - - def visit_section(self, node: Element) -> None: - names = node.attributes.get("names", []) - self.section_path.append(names[0].replace(" ", "") if names else "") - - def depart_section(self, node: Element) -> None: - self.section_path.pop() - - def unknown_visit(self, node: nodes.Node) -> None: - pass - - def unknown_departure(self, node: nodes.Node) -> None: - pass - - -class LobsterBuilder(TextBuilder): - name = "lobster" - format = "json" - epilog = __("The text files are in %(outdir)s.") - - out_suffix = ".json" - allow_parallel = True - default_translator_class = LobsterTranslator - - def finish(self) -> None: - super().finish() - # Merge all per-document .json files written by write_doc() into a - # single lobster implementation-trace file so consumers don't need a - # separate merge step. - merged: dict = { - "data": [], - "generator": "lobster_sphinx", - "schema": "lobster-imp-trace", - "version": 3, - } - for json_file in sorted(Path(self.outdir).rglob("*.json")): - with open(json_file, encoding="utf-8") as fh: - doc = json.load(fh) - merged["data"].extend(doc.get("data", [])) - # NOTE: The output filename "_merged.lobster" is referenced by the - # Bazel rule in tools/lobster/lobster_sphinx.bzl (_lobster_sphinx_impl). - # Both names must stay in sync. - merged_path = path.join(self.outdir, "_merged.lobster") - with open(merged_path, "w", encoding="utf-8") as fh: - json.dump(merged, fh, indent=4) - - -def setup(app: Sphinx): - app.add_builder(LobsterBuilder) - - return { - "version": "0.1", - "parallel_read_safe": True, - "parallel_write_safe": True, - }